1

I'm trying to improve the performance of an Application with multiple Tableviews that are constantly being updated. On each update only 1 item inside the ObservableList changes. The reloading of the rows of those Tableviews and applying css-styles to these rows are slowing down the application in a significant way.

I looked at the way, that Tableviews are reloading their rows and realized that all rows are constantly being reconstructed from scratch independent of the type of change to the underlying ObservableList. The changing of 1 item inside the viewport / 1 item outside of the viewport / a complete replacement of the List causes the exact number of row constructions.

Is there a way prevent the Javafx Tableviews from reloading all displayed Rows, when only certain Items change?

Other possible tweaks to improve the performance of constantly reloading Tableviews would be greatly appreciated.

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class FXApplication extends Application {

    private BorderPane border;
    private TableView<Item> tableView;
    private Button addItemToTv = new Button("Add");
    private Button replaceFirst = new Button("Replace first");
    private Button replaceLast = new Button("Replace last");
    private Button fullReplace = new Button("Full replace");

    private int counter = 0;

    protected Map<Integer, TableRow<Item>> rowsByIndex = new HashMap<>();

    @Override
    public void start(Stage primaryStage) {

        constructTv();
        setButtonActions();
        Scene s = constructScene();

        primaryStage.setScene(s);
        primaryStage.show();
    }

    private Scene constructScene() {
        border = new BorderPane();
        border.setCenter(tableView);
        border.setTop(new HBox(addItemToTv, replaceFirst, replaceLast, fullReplace));

        ScrollPane scp = new ScrollPane();
        scp.setFitToHeight(true);
        scp.setFitToWidth(true);
        scp.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
        scp.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
        scp.setContent(border);

        return new Scene(scp);
    }

    private void setButtonActions() {
        addItemToTv.setOnAction(e -> {
            tableView.getItems().add(new Item("bla"));
            Platform.runLater(this::resetCounter);
        });
        replaceFirst.setOnAction(e -> {
            tableView.getItems().set(0, new Item("blub"));
            Platform.runLater(this::resetCounter);
        });
        fullReplace.setOnAction(e -> {
            ArrayList<Item> items = new ArrayList<>(tableView.getItems());
            tableView.getItems().setAll(items);
            Platform.runLater(this::resetCounter);
        });
        replaceLast.setOnAction(e -> {
            tableView.getItems().set(tableView.getItems().size()-1, new Item("blub"));
            Platform.runLater(this::resetCounter);
        });
    }


    private void resetCounter() {
        Timeline tl = new Timeline(new KeyFrame(Duration.millis(500), (ee) -> {
            System.out.println(counter + " row calls");
            counter = 0;
        }));
        tl.play();

    }

    private Node constructTv() {
        tableView = new TableView<>();
        for(int i = 0; i<10; i++){
            TableColumn<Item, String> col = new TableColumn<>("col " + i);
            col.setCellValueFactory(param -> {
                return new SimpleStringProperty(param.getValue().getString());
            });
            tableView.getColumns().add(col);
        }

        for (int i = 0; i < 30 ; i++) {
            tableView.getItems().add(new Item("bla"));
        }

        tableView.setRowFactory(param -> {
            TableRow<Item> row = new TableRow<>();
            row.itemProperty().addListener((observable, oldValue, newValue) -> {
                Platform.runLater(() -> {
                    rowsByIndex.put(row.getIndex(), row);
                    System.err.println("row change " + row.getIndex());
                    counter++;
                });
            });
            return row;
        });

        return tableView;
    }


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

}
class Item {
    private String string;

    public Item(String s) {
        string = s;
    }

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }
}
craigcaulfield
  • 3,381
  • 10
  • 32
  • 40
stefan
  • 73
  • 3
  • 1
    It does not make sense to use `Platform.runLater` with `Timeline`. `Platform.runLater(this::resetCounter);` is useless. Also, you are basically creating new Timelines each time you press a button and all of them are running. That's like the opposite of high performance code (maybe not, but are using a bunch of unnecessary resources). Why the row listener in the cell factory? Why platform.runlater in the cell factory. I feel like everything you are doing is counter to the idea of performance. You need to start with some basic JavaFX tutorials. – SedJ601 Oct 21 '19 at 02:33
  • This is just a dummy programm to show the number of times that the RowListener gets called when applying changes to the ObservableList – stefan Oct 21 '19 at 08:19
  • yeah, we understand that this is an example - but as @Sedrick already noted: it looks like you are doing all to artificially slow down everything ;) Repeating: there is no need whatever to call Platform.runlater at any of the locations you use it here. Did you really profile your application and which bottleneck did it reveal? – kleopatra Oct 21 '19 at 08:58
  • ok i agree that the Platform.runlater is obsolete / harmful. Yes i did profile the application and the bottleneck was inside javafx.scene.control.skin.VirtualFlow.layoutChildren(). This then calls internal Methods setCellIndex() and applyCss(). I figured the best way to limit these internal calls would be to not construct 50 new rows for every 1 item change. – stefan Oct 21 '19 at 09:31
  • Note that you do not measure the creation of new rows here. If I'm not terribly mistaken `TableView` reuses `TableCell`s and `TableRow`s; it just updates them. Depending on the `TableCell` implementations this may be more or less expensive. Depending on the exact details of your application removing `TableView`s that are outside of the viewport may increase the performance. If that doesn't work, you may be required to reimplement the skin for `TableView`. Note that inserting 1 element does indeed require an css update, since different css is applied to odd and even rows... – fabian Oct 21 '19 at 17:27
  • In theory if i only change the first item of the ObservableList then only the first row has to be reloaded. The css of the others wont change. Likewise if i edit a item that is not currently displayed in the viewport then no rows would habe to be reloaded. ( Yes rows are recycled and there is one for every displayed item in the viewport, not for every item in the list. So there never was a row for the item outside of the viewport whose edit calls for all current rows to be recycled ) – stefan Oct 22 '19 at 07:47

0 Answers0