Skip to content

Conversation

@Maran23
Copy link
Member

@Maran23 Maran23 commented Jun 15, 2025

When calling refresh() on virtualized Controls (ListView, TreeView, TableView, TreeTableView), all cells will be recreated completely, instead of just refreshing them.

This is because recreateCells() of the VirtualFlow is called when refresh() was called. This is not needed, since refreshing the cells can be done much cheaper with rebuildCells().

This will reset all cells (index = -1), add them to the pile and fill them back in the viewport with an index again. This ensures updateItem() is called.

The contract of refresh() is also a big vague, stating:

Calling {@code refresh()} forces the XXX control to recreate and repopulate the cells 
necessary to populate the visual bounds of the control.
In other words, this forces the XXX to update what it is showing to the user. 
This is useful in cases where the underlying data source has changed in a way that is not observed by the XXX itself.

As written above, recreating is not needed in order to fulfull the contract of updating what is shown to the user in case the underlying data source changed without JavaFX noticing (e.g. calling a normal Setter without any Property and therefore listener involved).


Benchmarks

Setup:

  • TableView
  • 100 TableColumns
  • 1000 Items

Calling refresh() with a Button press.

WHAT BEFORE AFTER
Init 0ms 0 ms
Init 0ms 0 ms
Init 251 ms 246 ms
Init 47 ms 50 ms
Init 24 ms 5 ms
Refresh Nr. 1 238 ms 51 ms -> 0ms
Refresh Nr. 2 282 ms 35 ms -> 0ms
Refresh Nr. 3 176 ms 36 ms -> 0ms
Refresh Nr. 4 151 ms 39 ms -> 0ms
Refresh Nr. 5 156 ms 34 ms -> 0ms
Benchmark code
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class FXBug {

    public static void main(String[] args) {
        Application.launch(FxApp.class, args);
    }

    public static class FxApp extends Application {

        @Override
        public void start(Stage primaryStage) {
            TableView<String> tv = new TableView<>();
            tv.setSkin(new CTableViewSkin<>(tv));

            for (int i = 0; i < 1000; i++) {
                tv.getItems().add("str: " + i);
            }

            for (int index = 0; index < 100; index++) {
                TableColumn<String, String> tc = new TableColumn<>("title: " + index);
                tc.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue()));

                tv.getColumns().add(tc);
            }

            BorderPane root = new BorderPane();
            root.setCenter(tv);

            Button button = new Button();
            button.setOnAction(_ -> tv.refresh());
            root.setBottom(new HBox(button));

            Scene scene = new Scene(root, 1840, 1000);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    }

    private static class CTableViewSkin<T> extends TableViewSkin<T> {

        public CTableViewSkin(TableView<T> control) {
            super(control);
        }

        @Override
        protected VirtualFlow<TableRow<T>> createVirtualFlow() {
            return new CVirtualFlow<>();
        }
    }

    private static class CVirtualFlow<S extends IndexedCell> extends VirtualFlow<S> {

        @Override
        protected void layoutChildren() {
            long l = System.nanoTime();
            super.layoutChildren();
            System.out.println("Took: " + ((System.nanoTime() - l) / 1_000_000) + " ms");
        }
    }

}

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 2 Reviewers)
  • Change requires CSR request JDK-8369154 to be approved

Issues

  • JDK-8359599: Calling refresh() for all virtualized controls recreates all cells instead of refreshing the cells (Enhancement - P4)
  • JDK-8369154: Calling refresh() for all virtualized controls recreates all cells instead of refreshing the cells (CSR)

Reviewers

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1830

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

Using diff file

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

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Jun 15, 2025

👋 Welcome back mhanl! 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 Jun 15, 2025

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

@openjdk openjdk bot added the rfr Ready for review label Jun 15, 2025
@mlbridge
Copy link

mlbridge bot commented Jun 15, 2025

Webrevs

@kevinrushforth
Copy link
Member

This is a risky change and will need to be carefully tested.

Reviewers: @andy-goryachev-oracle @johanvos

/reviewers 2 reviewers

@openjdk
Copy link

openjdk bot commented Jun 16, 2025

@kevinrushforth
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 2 Reviewers).

@johanvos
Copy link
Collaborator

It is indeed true that refresh() is often a very expensive operation. Whenever VirtualFlow.recreateCells() is called, most of the internal state of the VirtualFlow is destroyed, and everything is recalculated from scratch. I believe that is not a problem, as long as recreateCells() is not called (by the controls) unless really needed. In that light, this PR seems interesting, as it will limit the number of rebuild-from-scratch cases.
I ran the basic controls tests after applying the PR, and (a bit to my surprise) they all passed, which is great.
However, it is very likely that some code out there implicitly rely on the rebuild-from-scratch logic, and that code will then work different after applying this PR. I believe it would be good to find such a case where the behavior (which, I agree, is often a bit vaguely defined) changes, so that we can discuss this with a concrete example.

Hence, while I like the idea here (avoiding unneeded heavy-cost operations in VirtualFlow), I would like to avoid a number of follow-up issues once this is merged -- driven by a change in "expected" behavior.

@Maran23
Copy link
Member Author

Maran23 commented Jun 18, 2025

Hence, while I like the idea here (avoiding unneeded heavy-cost operations in VirtualFlow), I would like to avoid a number of follow-up issues once this is merged -- driven by a change in "expected" behavior.

I completely agree. We need to be careful with such changes.

However, it is very likely that some code out there implicitly rely on the rebuild-from-scratch logic, and that code will then work different after applying this PR.

Since all rows (and cells) are reset and then updated, all changes that were not taken into account by the control are taken into account in any case then.
On a side note here: In any JavaFX project, I have overwritten the refresh method (since it is not final) and always implemented the lightweight method as proposed here. I never found any problem.

But I think there is one concrete case which breaks now.
Take the following example (slightly modified version of the code in the ticket):

Example
import java.util.concurrent.atomic.AtomicBoolean;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TableRefresh extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        TableView<String> tv = new RefreshTableView<>();
        tv.setEditable(true);
        AtomicBoolean alternate = new AtomicBoolean(false);

        tv.setRowFactory(e -> {
            if (alternate.get()) {
                System.out.println("Creating alternative row");
                return new TableRow<>() {
                    {
                        setPadding(new Insets(5));
                    }
                };
            }

            System.out.println("Creating row");
            return new TableRow<>();
        });
        TableColumn<String, String> col = new TableColumn<>("Name");
        col.setCellValueFactory(i -> new SimpleStringProperty(i.getValue()));

        tv.getColumns().addAll(col);

        tv.setItems(FXCollections.observableArrayList("A", "B"));
        VBox.setVgrow(tv, Priority.ALWAYS);
        Button btn = new Button("Refresh");
        btn.setOnAction(e -> {
            alternate.set(true);
            tv.refresh();
        });
        Scene scene = new Scene(new VBox(tv, btn));

        stage.setScene(scene);
        stage.setTitle("Table Refresh");
        stage.show();
    }
}

Here, I'm aware that refresh() is recreating all rows, and update a boolean flag before calling refresh(), which leads to another path that is picked up and therefore other rows.
In this case, it is much better to just set a new TableRow factory. This will do the same as what refresh() is doing right now (but not after this PR).
In never saw this code in the wild though.

But that is still a valid risk that we need to consider. This is the only problem I see right now.

@bridgekeeper
Copy link

bridgekeeper bot commented Jul 16, 2025

@Maran23 This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a /touch or /keepalive command to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@andy-goryachev-oracle
Copy link
Contributor

My main concern with this PR is that we might get a minute performance improvement, while risking regression. Would it be possible to get some measurements using a modern system?

What do you think?

@johanvos
Copy link
Collaborator

johanvos commented Aug 1, 2025

I think the performance improvements due to this PR can be pretty significant. The problem is indeed the risk on regression. I believe we need improvements in 2 areas before we can safely merge this:

  1. More functional regression tests
  2. Performance tests.

@hjohn
Copy link
Collaborator

hjohn commented Aug 1, 2025

I think a CSR is needed if we're changing the documentation. It specifically says it recreates the cells (although the purpose of that eludes me, aside from badly written cells).

Since cells are supposed to be updated, and not "retain" anything from previous contents, I think only only buggy cells would benefit from recreating. Such buggy cells however would probably have subtle problems already when they're never recreated, like listener leaks.

So although I agree that recreating is not needed as cell functionality is defined by their update methods, I do think this then requires a CSR.

@kevinrushforth
Copy link
Member

So although I agree that recreating is not needed as cell functionality is defined by their update methods, I do think this then requires a CSR.

I agree.

/csr needed

@openjdk openjdk bot added the csr Need approved CSR to integrate pull request label Aug 1, 2025
@openjdk
Copy link

openjdk bot commented Aug 1, 2025

@kevinrushforth has indicated that a compatibility and specification (CSR) request is needed for this pull request.

@Maran23 please create a CSR request for issue JDK-8359599 with the correct fix version. This pull request cannot be integrated until the CSR request is approved.

@bridgekeeper
Copy link

bridgekeeper bot commented Aug 29, 2025

@Maran23 This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a /touch or /keepalive command 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 Sep 27, 2025

@Maran23 This pull request has been inactive for more than 8 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 Sep 27, 2025
@Maran23
Copy link
Member Author

Maran23 commented Sep 30, 2025

/open

Will do the CSR + try to benchmark when I have more time!

@openjdk openjdk bot reopened this Sep 30, 2025
@openjdk
Copy link

openjdk bot commented Sep 30, 2025

@Maran23 This pull request is now open

@Maran23
Copy link
Member Author

Maran23 commented Oct 2, 2025

I did some benchmarks and attached them to the description. Results look very good and is expected from what I measured myself when I implemented that.

Since we do not need to throw away all TableRows and TableCells, and then recreate them, we get a massive boost. Especially when we have many items (-> rows) and many columns.
If TableCells have special logic, like showing a graphic or styling, this will be even bigger.

@BlueGoliath
Copy link

BlueGoliath commented Oct 2, 2025

@Maran23 Not a developer but I'd just like to say thanks for this. For the longest time I thought it was because of lazy coding on my part(new ReadOnlyObjectWrapper instances instead of caching) but it turns out that even if you optimize your code, TableView's refresh method still performs terribly.

I'd like also to point out that the terrible performance isn't just from having 100s of rows. Comparing the CPU usage difference between viewing many updating Label(s) at once vs a single TableView shows a CPU usage increase(0.01%-0.03%ish to 0.06%-0.08%ish) on relatively extremely powerful modern hardware with just about a dozen rows.

But what's even worse is the garbage allocation rate. TableView in my application allocates entire MEGABYTES of garbage in an application that has a relatively low garbage allocation rate. This not only results in more GCs but could result in expanding the heap, resulting in more app memory usage.

Can this please get more attention?

Comment on lines -355 to -368
/** TreeTableView.refresh() must release all discarded cells JDK-8307538 */
@Test
public void cellsMustBeCollectableAfterRefresh() {
IndexedCell<?> row = VirtualFlowTestUtils.getCell(treeTableView, 0);
assertNotNull(row);
WeakReference<Object> ref = new WeakReference<>(row);
row = null;

treeTableView.refresh();
Toolkit.getToolkit().firePulse();

JMemoryBuddy.assertCollectable(ref);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there another test that verifies that cells are garbage collectable? For example, in the case where a table / list / tree table becomes smaller visually, I think that it should then perhaps discard some cells that then should be collectable?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think there are some when switching the TableRow, as this should remove all old rows and gc them at one point.

Other than that, I think there is no case where we gc cells. When we change the viewport width/height, all rows (cells if a fixedCellSize is set) will be piled / cached, but not destroyed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alright, as long as there are still some GC checks I think this would be fine. Not sure if I agree with keeping cells/rows around that exceed the amount that are currently displayed, but that's how it already works (my own virtualized controls will only reference the cells displayed, but with non-fixed size cells/rows this may not be optimal).

Copy link
Collaborator

@hjohn hjohn left a comment

Choose a reason for hiding this comment

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

I think the changes look good. I'm a bit confused in the performance table with what is meant with the 50 ms -> 0 ms in the "after" cases though?

@Maran23
Copy link
Member Author

Maran23 commented Oct 3, 2025

I think the changes look good. I'm a bit confused in the performance table with what is meant with the 50 ms -> 0 ms in the "after" cases though?

Every refresh() will trigger 2 layouts for some reason, where the second one does nothing as nothing is dirty, so basically a noop. I can have a look into that (maybe as a follow up?) but I remember that this happens sometimes in general for the VirtualFlow and we should check that generally at one point.

@andy-goryachev-oracle solved that problem in the RichTextArea by isolating the method which should be called e.g. two times (due to e.g. ScrollBars) instead of a real relayout.

@hjohn
Copy link
Collaborator

hjohn commented Oct 4, 2025

I think the changes look good. I'm a bit confused in the performance table with what is meant with the 50 ms -> 0 ms in the "after" cases though?

Every refresh() will trigger 2 layouts for some reason, where the second one does nothing as nothing is dirty, so basically a noop. I can have a look into that (maybe as a follow up?) but I remember that this happens sometimes in general for the VirtualFlow and we should check that generally at one point.

No need to address that in this PR, I was just confused what the numbers meant (shouldn't the before column than not also have X ms -> 0 ms?). So it seems like quite a good performance improvement.

As a side note, even 30-40 ms seems incredibly slow, that's bound to create noticeable input lag or frame skips :/ How many cells were visible? 1000 or 100x1000? If the latter, than 30-40 ms seems okayish.

@Maran23
Copy link
Member Author

Maran23 commented Oct 5, 2025

As a side note, even 30-40 ms seems incredibly slow, that's bound to create noticeable input lag or frame skips :/ How many cells were visible? 1000 or 100x1000? If the latter, than 30-40 ms seems okayish.

I agree. One problem here is, that all cells will be updated (via updateItem) of a TableRow, even if not visible.

I counted all updateItem calls, results below.

Code from Benchmark above:

  • TableRow updateItem: 78
  • TableCell updateItem: 7800

39 rows are displayed, and they are updated twice (first with -1 to reset, then with the actual index). And all rows have 100 cells.

A TableCell updateItem without any code (no setText, no setGraphic) is indeed faster, around 10-20ms.
Looking into the code, there are some unnecessary requestLayout calls as well.

@BlueGoliath
Copy link

What needs to happen for this to get merged? Testing?

Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Choose a reason for hiding this comment

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

I don't see any obvious issues with the new code.

Performance wise, feels no different than the master branch. Using the monkey tester with 10,000 rows and 200 columns, both feel sluggish on vertical scrolling, which disappears once a fixedCellSize is set.

* Calling {@code refresh()} forces the TableView control to recreate and
* repopulate the cells necessary to populate the visual bounds of the control.
* Calling {@code refresh()} forces the TableView control to repopulate the
* cells necessary to populate the visual bounds of the control.
Copy link
Contributor

Choose a reason for hiding this comment

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

the word 'repopulate' is used twice (here and elsewhere)

I would suggest to rephrase these comments to indicate what happens exactly, possibly borrowing text from the VirtualFlow:

  • recreate: a layout pass should be done, and that the cell factory has changed. All cells in the viewport are recreated with the new cell factory.
  • rebuild: a layout pass should be done, and cell contents have changed. All cells are removed and then added properly in the viewport

Copy link
Member

Choose a reason for hiding this comment

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

At a minimum, replace the first occurrence of "repopulate" with "rebuild".

     * Calling {@code refresh()} forces the TableView control to rebuild the
     * cells necessary to populate the visual bounds of the control.

I wouldn't over-specify this by saying what VirtualFlow will do, but if you want to add a sentence saying that this will request a layout that would be fine:

     * Calling {@code refresh()} forces the TableView control to rebuild the
     * cells necessary to populate the visual bounds of the control.
     * This will request a layout of the TableView cells.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to rebuild. I did not add the request layout line, in case we may want to change this later. Since as @hjohn and @johanvos mentioned, it is rather weird right now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, though I would insist on actually explaining what "rebuild" means, as it is not clear from the context.

VirtualFlow offers more detailed explanation, so perhaps we should borrow that.

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe link to VirtualFlow? I'm not sure the details are important enough to repeat.

Copy link
Member

Choose a reason for hiding this comment

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

I think I see what you are getting at now. Let's take a step back.

What we want is to give the app developer enough information to know what the purpose of calling this method is, and what the effect will be. What we don't want to do is say how that is done. So you are right that we shouldn't appeal to VirtualFlow since it isn't really relevant in this context. Likewise, we don't want to constrain it with implementation details.

The refresh() method was added by JDK-8098085 to allow an app developer with a custom cell to say, in effect, "even if it doesn't look like anything is changed, get the contents of the cells anyway". If that's what updateItem() does, then yes we can say that. Otherwise, we need to find some other way to say it.

Copy link
Contributor

Choose a reason for hiding this comment

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

exactly, thank you @kevinrushforth

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed with Kevin yesterday, what do you think of this version:

     * Calling {@code refresh()} forces the XXX to update what it is showing to
     * the user. This is useful in cases where the underlying data source has
     * changed in a way that is not observed by the XXX itself.

Copy link
Member

Choose a reason for hiding this comment

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

That looks good. You might even simplify it further and remove Calling {@code refresh()}, since it's redundant, and just say Forces the XXX ...

Copy link
Member

Choose a reason for hiding this comment

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

@Maran23 Can you update the javadoc for the four methods as Andy suggested above (possibly with my simplification) and then update the CSR Specification section to match? I think that's the only remaining thing needed to move this forward.

} else if (Properties.RECREATE.equals(c.getKey())) {
needCellsRecreated = true;
refreshView();
if (Properties.RECREATE.equals(c.getKey())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I just have to raise this concern.

It looks weird to me to use properties as a roundabout way to invoke a hidden method in the skin. Node properties, a public facility, can be easily mutated by unrelated code, making it easy to break the intended functionality.

Why not make these two methods explicitly a part of the public API by replacing requestRebuildCells and requestRecreateCells with

protected VirtualContainerBase.rebuildCells()
protected VirtualContainerBase.recreateCells()

?

(requestXXX is a misnomer - it might suggest an async nature, whereas it these are synchronous methods)

Copy link
Member

Choose a reason for hiding this comment

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

Changing this is out of scope for this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Having said that, I've always disliked using user properties for this sort of thing. It feels like a bit of a hack, so it might be worth filing a follow-up to evaluate a different approach in general (not just for this one instance of the pattern). I don't see this as a high priority, though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding to what @kevinrushforth said, this is used everywhere. All layout constraints are implemented by using Properties. Changing anything there can break the layout.

If we just talking about the properties calling a method in the skin, I dislike the current way as well, but have no other idea really. Maybe a good topic for the mailing list then.

@kevinrushforth
Copy link
Member

@Maran23 Can you add the performance test program you ran to validate the performance numbers to this PR? A new directory under tests/performance seems a good place for it.

I'll review the CSR.

@johanvos had some comments earlier, so allow time for him to respond as well.

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.

I left one comment on the docs. I'll leave it to the other reviewers to test and review the implementation changes.

* Calling {@code refresh()} forces the TableView control to recreate and
* repopulate the cells necessary to populate the visual bounds of the control.
* Calling {@code refresh()} forces the TableView control to repopulate the
* cells necessary to populate the visual bounds of the control.
Copy link
Member

Choose a reason for hiding this comment

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

At a minimum, replace the first occurrence of "repopulate" with "rebuild".

     * Calling {@code refresh()} forces the TableView control to rebuild the
     * cells necessary to populate the visual bounds of the control.

I wouldn't over-specify this by saying what VirtualFlow will do, but if you want to add a sentence saying that this will request a layout that would be fine:

     * Calling {@code refresh()} forces the TableView control to rebuild the
     * cells necessary to populate the visual bounds of the control.
     * This will request a layout of the TableView cells.

@Maran23
Copy link
Member Author

Maran23 commented Oct 12, 2025

@Maran23 Can you add the performance test program you ran to validate the performance numbers to this PR? A new directory under tests/performance seems a good place for it.

Sure. I don't know how easy it is, but it might be worth to consider using JMH in the future. No idea if this is easy to wire up with JavaFX though.

@johanvos
Copy link
Collaborator

Every refresh() will trigger 2 layouts for some reason, where the second one does nothing as nothing is dirty, so basically a noop. I can have a look into that (maybe as a follow up?) but I remember that this happens sometimes in general for the VirtualFlow and we should check that generally at one point.

This is something that worries me. One of the main issues I see with e.g. VirtualFlow, is that some methods can be invoked both during a rendering pulse as well as (indirectly) via explicit invocations, most often by code invoked with Platform.runLater().
Depending on whether code is called during a pulse or not, the behavior can be very different. A major problematic consequence of a method that used to be called outside the pulse, and that is now for some reason (e.g. due to concurrency, as it depends whether the Runnable submitted to Platform.runLater() is executed before or after the pulse) called during a pulse, is that this can lead to flickering. If requestNextPulse is called during the layout phase, it is very well possible that a "wrong" layout is rendered briefly before the correct layout is shown.

I am not saying that this PR makes the situation worse or better, but I think there is a reasonable chance that some applications will behave differently (and show flickering). Having said that, I don't think that this PR can be blamed in case there is a different behavior.

@Maran23
Copy link
Member Author

Maran23 commented Oct 13, 2025

This is something that worries me. One of the main issues I see with e.g. VirtualFlow, is that some methods can be invoked both during a rendering pulse as well as (indirectly) via explicit invocations, most often by code invoked with Platform.runLater(). Depending on whether code is called during a pulse or not, the behavior can be very different. A major problematic consequence of a method that used to be called outside the pulse, and that is now for some reason (e.g. due to concurrency, as it depends whether the Runnable submitted to Platform.runLater() is executed before or after the pulse) called during a pulse, is that this can lead to flickering. If requestNextPulse is called during the layout phase, it is very well possible that a "wrong" layout is rendered briefly before the correct layout is shown.

I'm also not happy with the current situation. There are also some cases where cells request a layout while they are layouted. There is quite some room to optimize several scenarios here.
I would like to check this out and optimize at one point, but really afraid that there might be regressions. So this will probably take a while, but I have a far better understanding of the VirtualFlow and surrounding classes than e.g. a year ago + we have far more tests as well!

@Maran23
Copy link
Member Author

Maran23 commented Oct 13, 2025

There seems to be an intermediate test failure, maybe a race condition unrelated to this change:

TaskEventTest > cancelledCalledAfterHandler() FAILED
    org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
        at app//org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:183)
        at app//test.javafx.concurrent.TaskEventTest.cancelledCalledAfterHandler(TaskEventTest.java:410)

@kevinrushforth
Copy link
Member

There seems to be an intermediate test failure, maybe a race condition unrelated to this change:

Yes, it is already filed and tracked by JDK-8357459.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

csr Need approved CSR to integrate pull request rfr Ready for review

Development

Successfully merging this pull request may close these issues.

6 participants