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

8252936: Optimize removal of listeners from ExpressionHelper.Generic #108

Closed

Conversation

dannygonzalez
Copy link

@dannygonzalez dannygonzalez commented Feb 6, 2020

https://bugs.openjdk.java.net/browse/JDK-8185886

Optimisation to ExpressionHelper.Generic class to use Sets rather than Arrays to improve listener removal speed.

This was due to the removal of listeners in TableView taking up to 50% of CPU time on the JavaFX Application thread when scrolling/adding rows to TableViews.

This may alleviate some of the issues seen here:

TableView has a horrific performance with many columns #409
javafxports/openjdk-jfx#409 (comment)

JDK-8088394 : Huge memory consumption in TableView with too many columns
JDK-8166956: JavaFX TreeTableView slow scroll performance
JDK-8185887: TableRowSkinBase fails to correctly virtualise cells in horizontal direction

OpenJFX mailing list thread: TableView slow vertical scrolling with 300+ columns
https://mail.openjdk.java.net/pipermail/openjfx-dev/2020-January/024780.html


Progress

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

Issue

  • JDK-8252936: Optimize removal of listeners from ExpressionHelper.Generic

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 108

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

Using diff file

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

Webrev

Link to Webrev Comment

@bridgekeeper bridgekeeper bot added the oca Needs verification of OCA signatory status label Feb 6, 2020
@bridgekeeper
Copy link

bridgekeeper bot commented Feb 6, 2020

Hi dannygonzalez, welcome to this OpenJDK project and thanks for contributing!

We do not recognize you as Contributor and need to ensure you have signed the Oracle Contributor Agreement (OCA). If you have not signed the OCA, please follow the instructions. Please fill in your GitHub username in the "Username" field of the application. Once you have signed the OCA, please let us know by writing /signed in a comment in this pull request.

If you already are an OpenJDK Author, Committer or Reviewer, please click here to open a new issue so that we can record that fact. Please use "Add GitHub user dannygonzalez" as summary for the issue.

If you are contributing this work on behalf of your employer and your employer has signed the OCA, please let us know by writing /covered in a comment in this pull request.

@dannygonzalez
Copy link
Author

/signed

I have signed the Oracle Contributor Agreement today. Have not received back any confirmation yet though.

@bridgekeeper bridgekeeper bot added the oca-verify Needs verification of OCA signatory status label Feb 6, 2020
@bridgekeeper
Copy link

bridgekeeper bot commented Feb 6, 2020

Thank you! Please allow for up to two weeks to process your OCA, although it is usually done within one to two business days. Also, please note that pull requests that are pending an OCA check will not usually be evaluated, so your patience is appreciated!

@kleopatra
Copy link
Collaborator

hmm ... wouldn't the change violate spec of adding listeners:

If the same listener is added more than once, then it will be notified more than once.

@dannygonzalez
Copy link
Author

hmm ... wouldn't the change violate spec of adding listeners:

If the same listener is added more than once, then it will be notified more than once.

True, I hadn't read that spec in ObservableValueBase.
Although that does seem odd behaviour to me. Obviously as the original implementation was using an array I can see how the implementation drove that specification.

Non of the JavaFx unit tests test for that specific case as the unit tests all passed. It would be nice if there was a specific test case for this behaviour.

I would need to store a registration count for each listener to satisfy this requirement.

@kleopatra
Copy link
Collaborator

Although that does seem odd behaviour to me. Obviously as the original implementation was using an array I can see how the implementation drove that specification.

whatever drove it (had been so since the beginning ot java desktop, at least since the days of swing), there is no way to change it, is it?

Non of the JavaFx unit tests test for that specific case as the unit tests all passed. It would be nice if there was a specific test case for this behaviour.

yeah, the test coverage is ... not optimal :)

I would need to store a registration count for each listener to satisfy this requirement.

a count plus some marker as to where it was added:

addListener(firstL)
addListener(secondL)
addListener(firstL)

must result in firstL.invalidated, seconL.invalidated, firstL.invalidated .. which brings us back to .. an array?

@dannygonzalez dannygonzalez changed the title 8185886: Improve scrolling performance of TableView and TreeTableView WIP: Improve scrolling performance of TableView and TreeTableView Feb 12, 2020
@dannygonzalez dannygonzalez changed the title WIP: Improve scrolling performance of TableView and TreeTableView 8185886: Improve scrolling performance of TableView and TreeTableView Feb 12, 2020
@dannygonzalez
Copy link
Author

dannygonzalez commented Feb 12, 2020 via email

@kleopatra
Copy link
Collaborator

The listeners are called back in the order they were registered in my implementation although I didn’t see this requirement in the spec unless I missed something.

yeah, you are right can't find that spec on sequence right now - maybe I dreamed it :)

@dannygonzalez
Copy link
Author

/covered

@bridgekeeper
Copy link

bridgekeeper bot commented Feb 12, 2020

Thank you! Please allow for a few business days to verify that your employer has signed the OCA. Also, please note that pull requests that are pending an OCA check will not usually be evaluated, so your patience is appreciated!

@bridgekeeper bridgekeeper bot removed oca Needs verification of OCA signatory status oca-verify Needs verification of OCA signatory status labels Feb 17, 2020
@dannygonzalez
Copy link
Author

/covered

@bridgekeeper
Copy link

bridgekeeper bot commented Feb 17, 2020

You are already a known contributor!

@kevinrushforth
Copy link
Member

@dannygonzalez the reason for the jcheck failure is that you have commits with two different email addresses in your branch. At this point, it's probably best to squash the commits into a single commit with git rebase -i master (presuming that your local master is up to date), and then do a force-push.

ExpressionHelper.Generic now uses a Map for speed rather than linear
searching through an array.

We use a LinkedHashMap<Listener, Integer> in ExpressionHelper.Generic.
This ensures we can iterate through the map in insertion order to call back
listeners in the order they were registered (although this isn't a requirement
according to the spec, I got unit test failures when I used a HashMap instead).

It also allow us to keep track if the same listener has been registerd more than
once and hence honour the addListener and removeListener requirements.
Specifically:

addListener:
If the same listener is added more than once, then it will be notified more than once. That is, no check is made to ensure uniqueness.

removeLister:
If it had been added more than once, then only the first occurrence will be removed.

Check in unit tests to test the case where the same listener is registered/removed multiple times
@openjdk openjdk bot added the rfr Ready for review label Feb 18, 2020
@dannygonzalez
Copy link
Author

@kevinrushforth just a note to say there are other ExpressionHelper classes (i.e. MapExpressionHelper, SetExpressionHelper and ListExpressionHelper) that also use arrays and suffer from the linear search issue when removing listeners.

These however didn't appear in the critical path of the JavaFXThread and didn't come up in my profiling of TableView.

If this pull request is accepted, my opinion is that they should probably all move to using the same pattern as here, which is to use Maps instead of Arrays for their listener lists so that all these classes are uniform.

Thanks

@mlbridge
Copy link

mlbridge bot commented Feb 18, 2020

Webrevs

@yososs
Copy link

yososs commented Feb 22, 2020

Sorry for the interruption, send a PR that corrects the same problem.

Comment on lines +282 to +283
final Map<InvalidationListener, Integer> curInvalidationList = new LinkedHashMap<>(invalidationListeners);
final Map<ChangeListener<? super T>, Integer> curChangeList = new LinkedHashMap<>(changeListeners);
Copy link
Collaborator

Choose a reason for hiding this comment

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

You only need the entry set, so you don't need to copy the map, just the set.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, yes the EntrySet would make more sense here. I'll fix that up.

final Map<InvalidationListener, Integer> curInvalidationList = new LinkedHashMap<>(invalidationListeners);
final Map<ChangeListener<? super T>, Integer> curChangeList = new LinkedHashMap<>(changeListeners);

curInvalidationList.entrySet().forEach(entry -> fireInvalidationListeners(entry));
Copy link
Collaborator

Choose a reason for hiding this comment

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

The lambda can be converted to a method reference.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, I'll fix.

Predicate<Object> p = t -> t instanceof WeakListener &&
((WeakListener)t).wasGarbageCollected();

listeners.entrySet().removeIf(e -> p.test(e.getKey()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be listeners.keySet().removeIf(p::test);.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, will change.

Copy link
Author

Choose a reason for hiding this comment

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

Nope, actually:
listeners.keySet().removeIf(p::test)

is not the same as:
listeners.entrySet().removeIf(e -> p.test(e.getKey()));

We need to test against the entrySet.key not the entrySet itself.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to test against the entrySet.key not the entrySet itself.

I suggested to test against the elements in keySet(), which are the same as the ones in entrySet().getKey().

Copy link
Author

Choose a reason for hiding this comment

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

Gotcha, sorry I missed that.

Comment on lines +196 to +197
private int weakChangeListenerGcCount = 2;
private int weakInvalidationListenerGcCount = 2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are these set to 2 and why do you need them at all? The previous implementation needed to grow and shrink the array so it had to keep these, but Map takes care of this for you.

Copy link
Author

@dannygonzalez dannygonzalez Feb 24, 2020

Choose a reason for hiding this comment

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

I agree, I kept these in as I wasn't sure if there was a need to manually force the garbage collection of weak listeners at the same rate as the original implementation.
Removing this would make sense to me also.

Updated my thoughts on this, see my comments below.

Copy link
Author

Choose a reason for hiding this comment

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

As far as I know a LinkedHashMap doesn't automatically remove weak listener entries. The listener maps can be holding weak listeners as well as normal listeners.
So we need to keep the original behaviour from before where a count was checked on every addListener call and the weak references were purged from the array using the original calculation for this count.
Otherwise the map would never garbage collect these weak references.

The initial value of 2 for these counts was just the starting count although this is not necessarily strictly accurate. To be completely accurate then we would have to set the appropriate count in each constructor as follows:

i.e. in the Constructor with 2 InvalidationListeners:
weakChangeListenerGcCount = 0
weakInvalidationListenerGcCount = 2

in the Constructor with 2 ChangeListeners:
weakChangeListenerGcCount = 2
weakInvalidationListenerGcCount = 0

in the Constructor with 1 InvalidationListener and 1 ChangeListener:
weakChangeListenerGcCount = 1
weakInvalidationListenerGcCount = 1

Now, I could have used a WeakHashMap to store the listeners where it would automatically purge weak listeners but this doesn't maintain insertion order. Even though the specification doesn't mandate that listeners should be called back in the order they are registered, the unit tests failed when I didn't maintain order.

I am happy to remove this weak listener purge code (as it would make the code much simpler) but then we wouldn't automatically remove the weak listeners, but this may not be deemed a problem anyway?

Copy link
Collaborator

Choose a reason for hiding this comment

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

So we need to keep the original behaviour from before where a count was checked on every addListener call and the weak references were purged from the array using the original calculation for this count.

The GC'd weak listeners do need to be purged, but why is the original behavior of the counters still applicable?

Copy link
Author

Choose a reason for hiding this comment

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

Just wanted to keep a similar behaviour as before using the same calculation based originally on the size of the listeners array list and now based on the size of the map. So in theory the weak listeners should be trimmed at the same rate as before.
Happy to hear alternatives.

Copy link
Author

@dannygonzalez dannygonzalez Feb 24, 2020

Choose a reason for hiding this comment

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

Also bear in mind that the trimming of weak listeners is extremely slow as it has to iterate through each listener performing the weak listener test. We can't call this every time we add a listener as this would lock up the JavaFX thread completely (I tried it).
I assume this is why the original calculation was used where it backs of the rate the weak listener trimming code was called as the array list grew.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I honestly don't quite understand the old cleanup behavior of (oldCapacity * 3)/2 + 1. Why is it grown by x1.5? In your tests, can you try to change the cleanup threshold to higher and lower values and see what differences you get?

At the very least, the initial values of the counters should be set according to the specific constructor used.

Comment on lines +193 to +194
private Map<InvalidationListener, Integer> invalidationListeners = new LinkedHashMap<>();
private Map<ChangeListener<? super T>, Integer> changeListeners = new LinkedHashMap<>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Two comments on this:

  1. The old implementation initialized these lazily, so we might want to preserve that behavior. I think it is reasonable since in most cases an observable won't have both types of listeners attached to it.
  2. Since we're dealing wither performance optimizations here, we should think about what the initialCapcity and loadFactor of these maps should be, as it can greatly increase performance when dealing with a large amount of entries.

Copy link
Member

Choose a reason for hiding this comment

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

Adding to this, we need to ensure that the simple case of a few listeners doesn't suffer memory bloat. It may make sense to initially allocate a Map with a small capacity and load factor, and then reallocate it if someone adds more than a certain number of listeners.

Copy link
Author

Choose a reason for hiding this comment

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

Agree with the lazy initialisation and the initial setting of capacity and load factor to reduce memory footprint unless it's needed.

@kevinrushforth
Copy link
Member

I haven't done a detailed review yet, but I worry about the memory consumption and performance of using a Map for the simple cases where there is only a single (or very small number) of listeners. These methods are used in many more places than just the TableView / TreeTableView implementation.

@dannygonzalez
Copy link
Author

Replying to @nlisker and @kevinrushforth general comments about the memory consumption of using a LinkedHashMap:

I agree that at the very least these should be lazy initialised when they are needed and that we should initially allocate a small capacity and load factor.

We could also have some sort of strategy where we could use arrays or lists if the number of listeners are less than some threshold (i.e. keeping the implementation with a similar overhead to the original) and then switch to the HashMap when it exceeds this threshold. This would make the code more complicated and I wonder if this is the worth the extra complexity.

I know that in our application, the thousands of listeners unregistering when using a TableView was stalling the JavaFX thread.

I am happy to submit code that lazy initialises and minimises initial capacity and load factor as suggested or happy to take direction and suggestions from anyone with a deeper understanding of the code than myself.

@nlisker
Copy link
Collaborator

nlisker commented Feb 25, 2020

I think that a starting point would be to decide on a spec for the listener notification order: is it specified by their registration order, or that is it unspecified, in which case we can take advantage of this for better performance. Leaving is unspecified and restricting ourselves to having it ordered is losing on both sides.

@kevinrushforth
Copy link
Member

So I'd like to propose this third alternative, which would affect only VirtualFlow and the controls that use it, but wouldn't have any impact in the rest of controls as the other two options (as ExpressionHelper or Node listeners wouldn't be modified).

Given PR #185, which was mentioned above, (it isn't out for review yet, but I want to evaluate it), this would be a 4th approach.

As long as this really doesn't introduce a leak, it seems promising.

I note that these are not mutually exclusive.

We should discuss this on the list and not just in one or more of of the 3 (soon to be 4) open pull requests.

@kevinrushforth
Copy link
Member

@dannygonzalez Per this message on the openjfx-dev mailing list, I have filed a new JBS issue for this PR to use. Please change the title to:

8252936: Optimize removal of listeners from ExpressionHelper.Generic

@dannygonzalez dannygonzalez changed the title 8185886: Improve scrolling performance of TableView and TreeTableView 8252936: Optimize removal of listeners from ExpressionHelper.Generic Sep 9, 2020
@dannygonzalez
Copy link
Author

Thanks @kevinrushforth, I've changed the title.

@yososs
Copy link

yososs commented Sep 10, 2020

I have found that fixing this rudimentary problematic code alleviates your problem.

This fix will reduce CPU usage by about 1/3 without your changes.
This fix improves performance in many widespread use cases.

However, I'm wondering how to report the problem. Should it be handled in this issue? Should I deal with a new issue for a rudimentary issue?

@kevinrushforth What should i do?

@Override
public boolean removeAll(Collection<?> c) {
beginChange();
BitSet bs = new BitSet(c.size());
for (int i = 0; i < size(); ++i) {
if (c.contains(get(i))) {
bs.set(i);
}
}
if (!bs.isEmpty()) {
int cur = size();
while ((cur = bs.previousSetBit(cur - 1)) >= 0) {
remove(cur);
}
}
endChange();
return !bs.isEmpty();
}
@Override
public boolean retainAll(Collection<?> c) {
beginChange();
BitSet bs = new BitSet(c.size());
for (int i = 0; i < size(); ++i) {
if (!c.contains(get(i))) {
bs.set(i);
}
}
if (!bs.isEmpty()) {
int cur = size();
while ((cur = bs.previousSetBit(cur - 1)) >= 0) {
remove(cur);
}
}
endChange();
return !bs.isEmpty();
}

Rewritten so that BitSet is not used.

    @Override
    public boolean removeAll(Collection<?> c) {
    	if(this.isEmpty() || c.isEmpty()){
    		return false;
    	}
        beginChange();
        boolean removed = false;
        for (int i = size()-1; i>=0; i--) {
            if (c.contains(get(i))) {
                remove(i);
                removed = true;
            }
        }
        endChange();
        return removed;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
    	if(this.isEmpty() || c.isEmpty()){
    		return false;
    	}
        beginChange();
        boolean retained = false;
        for (int i = size()-1; i>=0; i--) {
            if (!c.contains(get(i))) {
                remove(i);
                retained = true;
            }
        }
        endChange();
        return retained;
    }

@kevinrushforth
Copy link
Member

@yososs Please file a new JBS issue for this. You will need to prove that your proposed change is functionally equivalent (or that any perceived changes are incidental and still conform to the spec). You should also think about whether your proposed change needs additional tests.

@yososs
Copy link

yososs commented Sep 10, 2020

Because it is such a small correction
Problem from me I feel that it is not easy to register, but I will try to register.

It has passed two existing tests for compatibility:

  • gradle base:test
  • gradle controls:test

I have just reported it as an enhancement proposal.

@openjdk
Copy link

openjdk bot commented Mar 17, 2021

@dannygonzalez Unknown command signed - for a list of valid commands use /help.

@openjdk
Copy link

openjdk bot commented Mar 17, 2021

@dannygonzalez Unknown command covered - for a list of valid commands use /help.

@openjdk
Copy link

openjdk bot commented Mar 17, 2021

@dannygonzalez Unknown command covered - for a list of valid commands use /help.

@Maran23
Copy link
Member

Maran23 commented Apr 1, 2021

Any news for this PR? Can I help somewhere?

@yososs
Copy link

yososs commented Apr 2, 2021

It seems that performance improvement PR often needs to make up for the lack of existing unit tests. However, this addition of unit tests should not be included in the same PR as it needs to be tested to meet the specifications before and after the change.

I think we need a step to fix the lack of unit tests before the performance improvement patch.

And care must be taken not to rely on internal implementations for this unit test so as not to hinder performance improvement. Some existing unit test code depends on the internal implementation, so some test changes may be needed.

@nadernader99
Copy link

nadernader99 commented Oct 1, 2021

This change is very near completion. According to @yososs, what remains is to split the work into two separate PRs. The first PR must contain the updated test covering additional cases to ensure backwards compatibility. The second should contain the performance enhancement changes. This will allow the tests to be merged into master first, to ensure the behavior is maintained before and after the performance enhancement.

I see that @dannygonzalez has not been able to continue this work. If possible and appropriate, I would like to finish this work. I have already setup a build environment and have verified that the updated tests alone pass when integrated with the master branch and have 100% coverage of the modified class.

@hjohn
Copy link
Collaborator

hjohn commented Oct 1, 2021 via email

@nadernader99
Copy link

The behavior is the same between openjdk 16.0.2 and 17.0.0.1. The test to recreate the problem adds many nodes (Rectangle) to a parent Group. The process causes each node to add two invalidation listeners (disabledProperty and treeVisibleProperty) to the parent node here using ExpressionHelper.Generic.addListener(). When the children are cleared from the group the inefficient linear search through the listener array and remove occurs, two linear searches per child.

Here is the minimal application I am using to recreate the issue.

public class ClearGroupPerformance extends Application {

    private static final int GAP = 10;
    private static final int SIZE = 40;
    private Group groupBase;

    public static void main(String[] args) {

        launch(args);
    }

    @Override
    public void start(Stage stage) {

        int width = 10000;
        int height = 10000;

        ScrollPane scrollPane = new ScrollPane();
        StackPane root = createGroupRoot(width, height);
        scrollPane.setContent(root);

        Button clearGroupButton = new Button("Clear Group");
        clearGroupButton.onActionProperty().set(e -> {

            long startTime = System.nanoTime();
            ObservableList<Node> children = groupBase.getChildren();
            int totalChildren = children.size();
            children.clear();
            System.out.printf("Clearing %d nodes took %.2fms%n", totalChildren, (System.nanoTime()-startTime)/1_000_000.0);

        });

        BorderPane borderPane = new BorderPane();
        borderPane.setTop(clearGroupButton);
        borderPane.setCenter(scrollPane);

        Scene scene = new Scene(borderPane, 100, 100);
        stage.setScene(scene);
        stage.show();

    }

    private StackPane createGroupRoot(int width, int height) {

        groupBase = new Group();
        groupBase.getChildren().add(new Rectangle(width, height, new Color(0.5, 0.5, 0.5, 1.0)));

        for (int posX = GAP; posX + SIZE + GAP < width; posX += SIZE + GAP) {
            for (int posY = GAP; posY + SIZE + GAP < height; posY += SIZE + GAP) {
                Rectangle rectangle = new Rectangle(posX, posY, SIZE, SIZE);
                rectangle.setFill(Color.GREEN);

                groupBase.getChildren().add(rectangle);
            }
        }

        return new StackPane(groupBase);
    }

}

@hjohn
Copy link
Collaborator

hjohn commented Oct 2, 2021

Thank you for the test case. I see that this is indeed another issue, although again one that seems to be triggered by an excessive amount of listeners being added by Node to something higher up in the hierarchy. Although I'm sure this is just a test to show the problem, work arounds would include organizing the rectangles in groups to avoid too many children under a single parent, or using Canvas.

Since the listeners added seem to be internal to Node to recursively keep track of disabled/visible state (and are not "optional", in other words, every child always registers them) it might be prudent to avoid this overhead and listener management completely. This could be achieved by having the parent Node pro-actively inform its children about changes without needing listeners, for example by just calling a private method. This should result in similar performance and would reduce memory overhead per node as there is no need for listener instances.

There is a 3rd property as well involved in the case there are transformations applied which could be given the same treatment (localToSceneTransformProperty).

@nadernader99
Copy link

Your points regarding unnecessary default listeners are valid.

The question now is should the work done on this PR be abandoned. It addresses a current performance limitation taking complexity from O(n log n) to O(1). It appears to be well isolated with low risk to backwards compatibility. The reason work was stopped was a concern that the tests should go first in a separate PR.

@kevinrushforth
Copy link
Member

Here is the minimal application I am using to recreate the issue.

I'll attach the test case you provided to the bug report.

The question now is should the work done on this PR be abandoned. It addresses a current performance limitation taking complexity from O(n log n) to O(1).

That is a fair question, but it begs two additional questions. Are there enough real world examples where performance is being hurt by using an ArrayList to justify the effort and risk (more on that below)? If so, are there other solutions that would reduce the number of listeners needed? The original problems that motivated this fix were largely addressed by JDK-8252935 / PR #185 and JDK-8252811 / PR #298.

It appears to be well isolated with low risk to backwards compatibility. The reason work was stopped was a concern that the tests should go first in a separate PR.

I disagree that this is a low risk change. This proposed fix touches a fundamental area used by all bindings, and does so in a way that will increase memory usage -- and might negatively impact performance -- in the common case of a small number of listeners. By unconditionally switching from an ArrayList to a HashSet, the memory required is significantly increased, which will be most noticeable for the common case of only a few listeners. An adaptive solution, which starts out using an ArrayList and dynamically switches to a LinkedHashSet (which preserves notification order...I know it isn't specified, but changing that is likely to break some applications) when crossing some threshold of the number of listeners, could mitigate this concern at the cost of added complexity.

If you still want to pursue this, you will need to do a fair amount of work to provide the additional tests that will be needed, to motivate the need for this enhancement (given that the original reasons are mostly addressed), and to resolve the issues that were raised in this PR. The mechanics of doing this are pretty easy. First, read the CONTRIBUTING guideline, specifically the part about signing the OCA, and create a new PR starting from this one. You would then add the author of this PR as a contributor.

In the mean time, I'm going to move this PR to Draft (which I should have done long ago), since it stalled and not being actively worked on or reviewed.

@kevinrushforth kevinrushforth marked this pull request as draft October 2, 2021 13:36
@kevinrushforth
Copy link
Member

/reviewers 3

@openjdk openjdk bot removed the rfr Ready for review label Oct 4, 2021
@openjdk
Copy link

openjdk bot commented Oct 4, 2021

@kevinrushforth
The number of required reviews for this PR is now set to 3 (with at least 1 of role reviewers).

@nadernader99
Copy link

@kevinrushforth makes an important point I was missing. Functionally this change is isolated but the memory overhead of a new data structure used by every node would risk performance degradation across many existing applications. I now see that the recommendation @hjohn to avoid adding unnecessary listeners by default should be prioritized over a faster listener lookup data structure. This will clarifies why work on this PR is currently back in draft status.

@openjdk
Copy link

openjdk bot commented Feb 17, 2023

@dannygonzalez this pull request can not be integrated into master due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

git checkout listeners_optimisation
git fetch https://git.openjdk.org/jfx master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push

@openjdk openjdk bot added the merge-conflict Pull request has merge conflict with target branch label Feb 17, 2023
@bridgekeeper
Copy link

bridgekeeper bot commented Apr 14, 2023

@dannygonzalez This pull request has been inactive for more than 8 weeks and will be automatically closed if another 8 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@bridgekeeper
Copy link

bridgekeeper bot commented Jun 10, 2023

@dannygonzalez This pull request has been inactive for more than 16 weeks and will now be automatically closed. If you would like to continue working on this pull request in the future, feel free to reopen it! This can be done using the /open pull request command.

@bridgekeeper bridgekeeper bot closed this Jun 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-conflict Pull request has merge conflict with target branch
Development

Successfully merging this pull request may close these issues.

9 participants