2

I created a gist which my problem is described in GraphGymnastic. The problem is heavily abstracted in there using Thread.sleep() but it serves the purpose.

Problem description

I want to walk the scenegraph when a event is filtered (with EventFilters). When an event arrives, I want to calculate some stuff on all nodes within the graph. For 2 nodes this will work just fine with Platform.runLater()but later, there will be n-Nodes and the calculation can take some time. I don't want the FX Thread blocked during this calculation.

So we thought about delegate the calculation to a second thread. Said, done. Now the problem we face is, we calculate on a second thread, but this thread holds references to the graph which can change in the meantime (we agree, this will not happen in most cases but it is to consider). So said, we calculate on a 'dynamic view'. What to do about this? Copy the graph? Then we are at the beginning, blocking the UI thread to copy the graph.

This is a pure design descision and I would be very thankful to hear of someone who has already done such things, if there are other ideas or approaches and if yes, how to solve this elegant not some construction site.

Thanks anyone who can help.

PS: In the gist, you must uncomment and comment the two lines with my comments to see the problem (after pressing button, try to move the window)

Edit: Insteed of gist, here is the code.

public class GraphGymnastic extends Application {
  final ExecutorService serv = Executors.newFixedThreadPool(2);
  public static void main(String argv[]) {
    launch(argv);
  }

  @Override public void start(Stage primaryStage) throws Exception {
    //Setup UI
    primaryStage.setTitle("Demo");
    final List<Node> nodesInGraph = new ArrayList<>();
    final FlowPane p = new FlowPane() {{
      setId("flowpane");
      getChildren().addAll(new Label("Label") {{
        setId("label");
      }}, new Button("Button") {{
        setId("button");
        // setOnMouseClicked(event -> handle(event, nodesInGraph)); //Uncomment and comment below to see effects!
        setOnMouseClicked(event -> handleAsync(event, nodesInGraph));
      }});
      setHgap(5);
    }};

    //Assume that this goes recursive and deep into a scene graph but still
    // returns a list
    //Here it takes the two childs for simplicity
    nodesInGraph.addAll(p.getChildrenUnmodifiable());

   //Show stage
    primaryStage.setScene(new Scene(p));
    primaryStage.show();
  }

  public void handle(MouseEvent ev, List<Node> nodesInGraph) {
    if (null != nodesInGraph)
      Platform.runLater(() -> nodesInGraph.forEach(node -> {
        //This will block the UI thread, so there is the need for a second
        //thread
        System.out.println(
            "Calculating heavy on node " + node.toString() + " with event from "
                + ev.getSource().toString());
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }));
  }

  public void handleAsync(MouseEvent ev, List<Node> nodesInGraph) {
    if (null != nodesInGraph)
      serv.submit(() -> nodesInGraph.forEach(node -> {
        //Now there is a second thread but it works on a LIVE view object
        // list, which is ugly
        //Option 1: Keep it like this, drawbacks? :S
        //Option 2: Copy the graph, costs performance... How deep should it
        // copy? :S
        System.out.println(
            "Calculating heavy on node " + node.toString() + " with event from "
                + ev.getSource().toString());
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }));
  }
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
mambax
  • 1,079
  • 8
  • 17
  • 1
    no gist please - all information should be available in the question itself – kleopatra Jul 24 '15 at 08:25
  • hmm .. see the problem. Strictly speaking, you must not _access_ the graph off the fx thread (can't find a reference, though, my memory might betray me), so doing the walking in another thread is fishy as well. What exactly is the time-consuming part, the walking or the calculation. Or how much are they interrelated? Could you extract some state on the fx-thread and then pass over that state to the other thread for processing? Just groping around a bit ... ;-) – kleopatra Jul 24 '15 at 09:17
  • The walking is done only once, afterwards its more of traversing a list. The calculation is(could) be the time consuming part. Since you agree that processing the list of nodes in the 2nd thread is kind of 'fishy' :) I assume a copy of the graph is the best approach no? If so, I struggle with: Either copy and use resources for copying then dispatch worker to calc or calculate on UI-Thread itself also using resources... :S – mambax Jul 24 '15 at 09:42
  • Just found this: https://books.google.ch/books?id=Wb8ICAAAQBAJ&lpg=PA1073&ots=_8eGVFc55_&dq=access%20scene%20graph%20from%20another%20thread&hl=de&pg=PA1073#v=onepage&q=accessing&f=false There would be an example of accessing the graph from another thread but it is hidden :S Page 1073 – mambax Jul 24 '15 at 09:46

1 Answers1

3

Not a concurrency expert, so this is just to illustrate my comment: extract some state on the fx-thread and then pass over that state to the other thread for processing

The ingredients:

  • a live list of nodes
  • a State object that can carry a snapshot of node properties (at the time of asking) and then does some lengthy process on it
  • a Task (as of fx concurrency): in the background thread, it creates a state object, switches over to the fx thread, copies relevant state, switches back to background, starts processing and sets its value when ready
  • a handler that listens to the task's value

The code:

// the worker
public static class SceneGraphWorker extends Task<NodeState> {

    private List<Node> nodes;
    private int current;

    public SceneGraphWorker(List<Node> nodes) {
        this.nodes = nodes;
    }

    @Override
    protected NodeState call() throws Exception {
        while (current >= 0) {
            NodeState state = new NodeState(current);
            CountDownLatch latch = new CountDownLatch(1);
            Platform.runLater(() -> {
                Node node = nodes.get(current);
                Bounds bounds = node.localToScene(node.getBoundsInLocal());
                state.setState(bounds.getMinX(), bounds.getMinY());
                state.setID(node.getId());
                current = current == nodes.size() - 1 ? -1 : current + 1;
                latch.countDown(); 
            });
            latch.await();
            state.process();
            updateValue(state);
        }
        return null;
    }

}

// the handler: listens to value
public void handleWithWorker(MouseEvent ev, List<Node> nodesInGraph) {
    Task worker = new SceneGraphWorker(nodesInGraph);
    worker.valueProperty().addListener((src, ov, nv) -> {
        progress.setText(nv != null ? nv.toString() : "empty");
    });
    new Thread(worker).start();
}

// the state object
public static class NodeState {
    double x, y;
    int index;
    private String name;
    public NodeState(int index) {
        this.index = index;
    }
    public void setState(double x, double y) {
        this.x = x;
        this.y = y;
    }
    public void setID(String name) {
        this.name = name;
    }
    public void process() throws InterruptedException {
        Thread.sleep(2000);
    }
    @Override
    public String toString() {
        return "[" + name + " x: " + x + " y: " + y + "]";
    }
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Works like a charm! *thumbup* First I thought what the hell did he do why is it so slow, then saw it was my crappy idea to imitate calculation with sleep :D Anyway, do I see it right that you box the state of the node so you dont have to copy it? You refer it as state? Then when all states are there you process them? – mambax Jul 24 '15 at 12:21
  • yeah, that's the idea - beware: the emphasis is on _idea_ like in I don't know if that's really feasable (might blow if you have many properties to copy), nor if that's the way-to-do-it .. – kleopatra Jul 24 '15 at 12:26
  • No, just the coordinates like you did! – mambax Jul 24 '15 at 12:27
  • One more comment after looking at your solution, why the Task? Why no ExecutorService? :S I mean I can just copy the NodeStates really fast then dispatch the executor...? Thank you for your explanation. – mambax Jul 24 '15 at 12:52
  • You can sumbit tasks to it, it will do what you give him as Runnable or Callable... Does your mechanism switch from Front to Background Thread more than once? Or just copy states than goto back? – mambax Jul 24 '15 at 13:02
  • switches back and forth once per node - you see the code in call, don't you :-) Hmm ... or maybe I don't fully understand what you are asking? call is on the background thread, content of runlater on ui thread – kleopatra Jul 24 '15 at 13:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/84187/discussion-between-m4mbax-and-kleopatra). – mambax Jul 24 '15 at 13:28
  • don't like to chat - and leaving into the weekend soon, anyway :-) – kleopatra Jul 24 '15 at 13:29