Tuesday, July 7, 2015

JavaFX: Working with Platform.runLater

In Java Swing background process can update GUI by registering Runnable with invokeLater() or invokeAndWait() in SwingUtilities class.   There is no equivalent of invokeAndWait in JavaFX's Platform class but there is a runLater() which is the same as invokeLater as far as method call is concern.  While Platform.runLater seems to be working fine it does not work well under stress conditions.   When trying to update thousand of changes to the GUI I will notice slowness  and none responsive behavior. Unfortunately it is not a bug as it is clearly noted in the method description in JavaFX 8 doc:

public static void runLater(Runnable runnable)
Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future. This method, which may be called from any thread, will post the Runnable to an event queue and then return immediately to the caller. The Runnables are executed in the order they are posted. A runnable passed into the runLater method will be executed before any Runnable passed into a subsequent call to runLater. If this method is called after the JavaFX runtime has been shutdown, the call will be ignored: the Runnable will not be executed and no exception will be thrown.
NOTE: applications should avoid flooding JavaFX with too many pending Runnables. Otherwise, the application may become unresponsive. Applications are encouraged to batch up multiple operations into fewer runLater calls. Additionally, long-running operations should be done on a background thread where possible, freeing up the JavaFX Application Thread for GUI operations.This method must not be called before the FX runtime has been initialized. For standard JavaFX applications that extend Application, and use either the Java launcher or one of the launch methods in the Application class to launch the application, the FX runtime is initialized by the launcher before the Application class is loaded. For Swing applications that use JFXPanel to display FX content, the FX runtime is initialized when the first JFXPanel instance is constructed. For SWT application that use FXCanvas to display FX content, the FX runtime is initialized when the first FXCanvas instance is constructed.
Parameters:
runnable - the Runnable whose run method will be executed on the JavaFX Application Thread
Throws:
IllegalStateException - if the FX runtime has not been initialized

Which is quite different from the JavaFX 2.2 doc:

public static void runLater(java.lang.Runnable runnable)
Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future. This method, which may be called from any thread, will post the Runnable to an event queue and then return immediately to the caller. The Runnables are executed in the order they are posted. A runnable passed into the runLater() method will be executed before any Runnable passed into a subsequent call to runLater().
Parameters:
runnable - the Runnable whose run method will be executed on the JavaFX Application Thread


From the differences between method descriptions we can clearly tell that there was a major restructuring on how the runLater() works.  This change adds complications to application design and decreases the FX's usability at the same time.  To make the application work properly, it must be designed to satisfy two requirement:

1. "Avoid flooding JavaFX with too many pending Runnables"
2. Long-running operations should not be in the Runnables

One way to "avoid flooding JavaFX with too many pending Runnables" is to create a throttling mechanism that sit on top of the runLater API to control the GUI update rate from the application.  Another way is to increase the queue size of the FX application thread with prioritized Runnables .  Either way is fairly easy to do.  The question is why isn't it part of the API itself.

Putting restriction on how long each Runnable should take is very undesirable in complex applications.  If an application registers two Runnables, A and B, to runLater() where B depends on the completion A.  A and B each has three proceedures, P1, P2 and P3 that must be executed in sequence where P1 and P3 are background process and P2 is the GUI updating process.  Such dependency will not allow the application to separate the proceedures P1-P3 in multiple threads, therefore, cannot sastisfy the second requirement.

Lets look at the solution for the first problem "Avoid flooding JavaFX with too many pending Runnables": Creating a queue to pace the amount of Runnables going into FX application thread.

class FXAppQueue extends Thread
{
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(6000);
public FXAppQueue()
{
}

public void add(Runnable r)
{
try
{
queue.put(r);
}
catch(Exception e)
{
}

}


public void run()
{
while(true)
{
 try
{
Runnable r = queue.take();
Platform.runLater(r);
try{ Thread.sleep(50); } catch(Exception e){} // This is to slow it down.
}
catch(Exception e)
{
}
}
}
}


Calling it from application:

....

Button button = new Button();
FXAppQueue fxQueue = new FXAppQueue();
fxQueue.start();
for(int i = 0; i < 5000; i++)
   fxQueue.add( ()->{ button.setText(String.valueOf(i)); });

...

2 comments:

  1. Chào bạn. Ấn tượng với kiến thức của bạn về JavaFX. Mình đang gặp vấn đề
    throttling JavaFX. Hy vọng có thể liên lạc được với bạn.

    ReplyDelete