8

In my JavaFX project I'm using a lot of shapes(for example 1 000 000) to represent geographic data (such as plot outlines, streets, etc.). They are stored in a group and sometimes I have to clear them (for example when I'm loading a new file with new geographic data). The problem: clearing / removing them takes a lot of time. So my idea was to remove the shapes in a separate thread which obviously doesn't work because of the JavaFX singlethread.

Here is a simplified code of what I'm trying to do:

HelloApplication.java

package com.example.javafxmultithreading;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {

    public static Group group = new Group();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load());
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();

        for (int i = 0; i < 1000000; i++) {
            group.getChildren().add(new Line(100, 200, 200, 300));
        }
        HelloController.helloController = fxmlLoader.getController();
        HelloController.helloController.pane.getChildren().addAll(group);
    }

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

HelloController.java

public class HelloController {

    public static HelloController helloController;
    @FXML
    public Pane pane;
    public VBox vbox;

    @FXML
    public void onClearShapes() throws InterruptedException {
        double start = System.currentTimeMillis();
        HelloApplication.group.getChildren().clear();
        System.out.println(System.currentTimeMillis() - start);

        Service<Boolean> service = new Service<>() {
            @Override
            protected Task<Boolean> createTask() {
                return new Task<>() {
                    @Override
                    protected Boolean call() {
                        // Try to clear the children of the group in this thread
                        return true;
                    }
                };
            }
        };
        service.setOnSucceeded(event -> {
            System.out.println("Success");
        });
        service.start();
    }
}

hello-view.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox fx:id="vbox" alignment="CENTER" prefHeight="465.0" prefWidth="711.0" spacing="20.0"
      xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="com.example.javafxmultithreading.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>
    <Pane fx:id="pane" prefHeight="200.0" prefWidth="200.0"/>
    <Button mnemonicParsing="false" onAction="#onClearShapes" text="Clear shapes"/>
</VBox>

I measured the time it took to remove different amounts of children from the group with group.getChildren().clear():

amount of children    |   time
100                       2ms = 0,002s
1 000                     4ms = 0,004s
10 000                    38ms = 0,038s
100 000                   1273ms = 1,2s
1 000 000                 149896ms = 149,896s = ~2,5min

As you can see, the time required increases exponentially.. And now imagine you have to clear the children in an UI and the user has to wait 2,5min for the application while it's freezing. Additionally, in this simplified example it's just a simple line, in the "real" application it's a more complicated geometry -> needs more time.

So another idea was to 'unbind' the group from it's parent, the pane. Because when it's unbind, I can remove it in another thread. That means 1. the ui doesn't freeze and 2. it will be faster. This was the try:

pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above

The problem: this 'unbinding' takes also a lot of time. Not 2,5min, but like 0,5min, which is still to much.

Another idea was to create multiple groups, because as you can see, a group with 10 000 or 100 000 elements is cleared faster. That also failed because several groups suddenly take longer and are deleted exponentially faster. For example, the first takes 20 seconds, the second 10, the third 5, etc.

Long story short

Is there any chance to remove the children of the group in a seperate thread or faster than with group.getChildren().clear()? I tried everything that comes to my mind...

And if I could only show a loading bar while deleting, it would be better than just freezing the surface and waiting 2min...

I appreciate every idea / help.

EDIT, see comments Simple example without FXML:

import javafx.scene.Group;
import javafx.scene.shape.Line;

public class Test {

    public static void main(String[] args) {
        Group group = new Group();
        System.out.println("adding lines");
        for (int i = 0; i < 1000000; i++) {
            group.getChildren().add(new Line(100, 200, 200, 300));
        }
        System.out.println("adding done");

        System.out.println("removing starts");
        double start = System.currentTimeMillis();
        group.getChildren().clear();
        System.out.println("removing done, needed time: " + (System.currentTimeMillis() - start));
    }
}
Enrico
  • 415
  • 1
  • 10
  • 1
    It seems unlikely that a simple call to `clear()` would really take that long. (That would likely indicate some kind of bug.) Note that you cannot modify live nodes (nodes that are part of the scene graph) from a background thread, so I don’t really see the threading would help at all here. Can you post a complete, simple example we can copy and run with a simple attempt to do this (and without all the static fields, etc)? I can’t see why a standard approach to this wouldn’t work. – James_D Dec 07 '21 at 19:51
  • 1
    "a lot of shapes(for example 1 000 000) to represent geographic data" -> Just my opinion, but I don't think this is a good design. Shapes are nodes, which are designed for screen rendering. There is an overhead in using a node to represent this kind of data rather than a data structure specifically targeted for that type of data. I don't know GIS systems, but I am sure they have data representations with Java bindings which would be more efficient for this kind of data than using nodes. Eventually, you need to map to visible nodes, but it should not be 1 million. – jewelsea Dec 07 '21 at 19:54
  • However, if your design is efficient enough for your needs, I guess don't worry. – jewelsea Dec 07 '21 at 19:54
  • @James_D This is the complete example.. how should i simplify it? Only idea is to do it without FXML – Enrico Dec 07 '21 at 20:00
  • @jewelsea yes you could be right ... 1,000,000 is also set high, but right now I want to use shapes from JavaFX, maybe something for later to optimize – Enrico Dec 07 '21 at 20:01
  • 2
    I just don’t understand why you are defining the `Group` as public and static, and in the `Application` class, and then manipulating the nodes in the controller from there. That’s not “normal” JavaFX (or even Java) programming style. The further you get from standard practices, the more likely it is you fall into some weird corner case which impacts performance for some strange reason. (Though it seems unlikely that this is the cause here.) Anyway, I’ll experiment when back at the computer – James_D Dec 07 '21 at 20:08
  • @James_D I edited my post and added a simple example, did you mean something like that? – Enrico Dec 07 '21 at 20:14
  • Yes, thanks; that's helpful. And also surprising. It looks like there are, maybe, some listeners on the child list that are doing far too much work in this case. – James_D Dec 07 '21 at 20:30
  • 1
    @James_D From profiling, the time seems to be spent in the `invalidated()` method of the `parent` property (the parent is set to null when a child is removed). In particular, removing listeners from the _parent's_ `treeVisible` and `disabled` properties seems to be expensive. – Slaw Dec 07 '21 at 21:36
  • 2
    There are 1,000,000 children which means these properties each have at least 1,000,000 listeners registered. These listeners are stored in an array and are removed _one by one_ when all 1,000,000 children are cleared. That means the work is _at least_ 1,000,000 linear searches **per property** (granted, the search time decreases as the removed children are processed, but still). – Slaw Dec 07 '21 at 21:45

1 Answers1

9

The long execution time comes from the fact that each child of a Parent registers a listener with the disabled and treeVisible properties of that Parent. The way JavaFX is currently implemented, these listeners are stored in an array (i.e. a list structure). Adding the listeners is relatively low cost because the new listener is simply inserted at the end of the array, with an occasional resize of the array. However, when you remove a child from its Parent and the listeners are removed, the array needs to be linearly searched so that the correct listener is found and removed. This happens for each removed child individually.

So, when you clear the children list of the Group you are triggering 1,000,000 linear searches for both properties, resulting in a total of 2,000,000 linear searches. And to make things worse, the listener to be removed is either--depending on the order the children are removed--always at the end of the array, in which case there's 2,000,000 worst case linear searches, or always at the start of the array, in which case there's 2,000,000 best case linear searches, but where each individual removal results in all remaining elements having to be shifted over by one.

There are at least two solutions/workarounds:

  1. Don't display 1,000,000 nodes. If you can, try to only display nodes for the data that can actually be seen by the user. For example, the virtualized controls such as ListView and TableView only display about 1-20 cells at any given time.

  2. Don't clear the children of the Group. Instead, just replace the old Group with a new Group. If needed, you can prepare the new Group in a background thread.

    Doing it that way, it took 3.5 seconds on my computer to create another Group with 1,000,000 children and then replace the old Group with the new Group. However, there was still a bit of a lag spike due to all the new nodes that needed to be rendered at once.

    If you don't need to populate the new Group then you don't even need a thread. In that case, the swap took about 0.27 seconds on my computer.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thank you very much for the detailed answer. How should I replace the `Group`? Like in my FXML example above, there is a `Pane` with a `Group`. If I try to replace the remove statement with `HelloApplication.group = new Group();` the old group is still a child of the `Pane` and the new one isn't. – Enrico Dec 08 '21 at 07:23
  • You have to remove the old group and add the new one. If your group is the only child of the pane, then you can simply do `pane.getChildren().setAll(newGroup)`. – Slaw Dec 08 '21 at 07:37
  • Ok this works very fast, thanks! Unfortunately, this doesn't work for my "real" code, where `setAll` still takes 73 seconds. But now it's up to my code, so I'll try something out and find out what the problem is. One more question: How can I look at the code from the Java libraries? So what happens, for example, when calling setAll? (I'm using Intellij) – Enrico Dec 08 '21 at 08:43
  • @Enrico You can download the sources for JavaFX too and attach them to your IDE or better instruct your Maven/Gradle build to download and attach the sources automatically. – mipa Dec 08 '21 at 09:13
  • 2
    Concerning you original problem I still don't know why you think you have to add that many individual nodes to the scene graph. I am in the geographic business too and I just don't see a valid use case for that. Have you considered Paths or other ways of representing your graphics elements? Could you show an example of what you actually want to draw? – mipa Dec 08 '21 at 09:18
  • @Enrico if you use idea+maven, you can synchronize dependency sources and doc [like this to do it automatically all the time](https://www.baeldung.com/maven-download-sources-javadoc#ide) or [like this](https://stackoverflow.com/a/29199629/1155209) to do it manually on demand. – jewelsea Dec 08 '21 at 09:21
  • @mipa At the moment I don't have a filter or something like that to remove nodes that are unnecessary (planned for the future) because they aren't visible. Another thing is, that I'm using only `Line`s instead of `Polyline`s, because a zoom and move functionality is implemented and definitely slower with polylines. Do you know if paths have a good performance? I will give it a try, thanks! In my example there are files with 200 000 shapes (lines, texts, arcs, circles, symbols...), but there are lines with e.g. 1000 points, so im creating 2000 lines instead of using one polyline...That adds up – Enrico Dec 08 '21 at 09:36
  • Polylines are actually a special kind of Paths internally and are thus equally slow. The only advantage is that you don't have to create than many nodes then. So, you are at the point where I was a couple of years ago. In order to cope with this problem I have written my own JavaFX framework which converts all Paths to triangle meshes and renders them truly on the graphics hardware via the TriangleMesh class of JavaFX. That way the zoom is super smooth and high quality. – mipa Dec 08 '21 at 09:45
  • @mipa Sounds interesting. How much work and how complex was that? Think I'm not able to this yet... – Enrico Dec 08 '21 at 10:26
  • @mipa _I have written my own JavaFX framework_ open source maybe? – kleopatra Dec 08 '21 at 11:30
  • Here are two links which show the result: https://www.dropbox.com/s/ek368itquamy6ve/line_mode.png?dl=0 , https://www.dropbox.com/s/9sye8g87ipzshkp/fill_mode.png?dl=0 Also the fine black outlines are all triangles. I might consider making this open source if there is enough interest in it. – mipa Dec 08 '21 at 11:53
  • I'm very interested in it and maybe it can help me out with the performance issues. Also other people can certainly benefit from the knowledge and maybe even contribute their knowledge... So if you make it open source, please let me know! – Enrico Dec 08 '21 at 13:29
  • I'll think about it. Maybe over Christmas. – mipa Dec 08 '21 at 14:57