JavaFX2 - very poor performance when adding custom made (fxml)panels to gridpane dynamically
Asked Answered
V

3

10

Problem I want to add custom made panels, built via javafx scene builder, to a gridpane at runtime. My custom made panel exsits of buttons, labels and so on.

My Attempt I tried to extend from pane...

public class Celli extends Pane{
    public Celli() throws IOException{
        Parent root = FXMLLoader.load(getClass().getResource("Cell.fxml"));
        this.getChildren().add(root);     
    }
}

... and then use this panel in the adding method of the conroller

@FXML
private void textChange(KeyEvent event) {
    GridPane g = new GridPane();
        for (int i=0 : i<100; i++){
                g.getChildren().add(new Celli());
        }
    }
}

It works, but it performs very very poor.

What I am looking for Is there a way to design panels via javafx scene builder (and as a result having this panels in fxml) and then add it to a gridpane at runtime without make use of this fxmlloader for each instance. I think it performs poor because of the fxml loader. When I add a standard button e.g. whitout fxml it is very much faster.

Vile answered 31/7, 2012 at 7:8 Comment(0)
I
23

Short answer: No, it is not (as of JavaFX 2.x and 8.0). It may be in a future version (JFX >8)

Long answer: The FXMLLoader is currently not designed to perform as a template provider that instantiates the same item over and over again. Rather it is meant to be a one-time-loader for large GUIs (or to serialize them).

The performance is poor because depending on the FXML file, on each call to load(), the FXMLLoader has to look up the classes and its properties via reflection. That means:

  1. For each import statement, try to load each class until the class could successfully be loaded.
  2. For each class, create a BeanAdapter that looks up all properties this class has and tries to apply the given parameters to the property.
  3. The application of the parameters to the properties is done via reflection again.

There is also currently no improvement for subsequent calls to load() to the same FXML file done in the code. This means: no caching of found classes, no caching of BeanAdapters and so on.

There is a workaround for the performance of step 1, though, by setting a custom classloader to the FXMLLoader instance:

import java.io.IOException; 
import java.net.URL; 
import java.util.Enumeration; 
import java.util.HashMap; 
import java.util.Map; 

public class MyClassLoader extends ClassLoader{ 
  private final Map<String, Class> classes = new HashMap<String, Class>(); 
  private final ClassLoader parent; 

  public MyClassLoader(ClassLoader parent) { 
    this.parent = parent; 
  } 

  @Override 
  public Class<?> loadClass(String name) throws ClassNotFoundException { 
    Class<?> c = findClass(name); 
    if ( c == null ) { 
      throw new ClassNotFoundException( name ); 
    } 
    return c; 
  } 

  @Override 
  protected Class<?> findClass( String className ) throws ClassNotFoundException { 
// System.out.print("try to load " + className); 
    if (classes.containsKey(className)) { 
      Class<?> result = classes.get(className); 
      return result; 
    } else { 
      try { 
        Class<?> result = parent.loadClass(className); 
// System.out.println(" -> success!"); 
        classes.put(className, result); 
        return result; 
      } catch (ClassNotFoundException ignore) { 
// System.out.println(); 
        classes.put(className, null); 
        return null; 
      } 
    } 
  } 

  // ========= delegating methods ============= 
  @Override 
  public URL getResource( String name ) { 
    return parent.getResource(name); 
  } 

  @Override 
  public Enumeration<URL> getResources( String name ) throws IOException { 
    return parent.getResources(name); 
  } 

  @Override 
  public String toString() { 
    return parent.toString(); 
  } 

  @Override 
  public void setDefaultAssertionStatus(boolean enabled) { 
    parent.setDefaultAssertionStatus(enabled); 
  } 

  @Override 
  public void setPackageAssertionStatus(String packageName, boolean enabled) { 
    parent.setPackageAssertionStatus(packageName, enabled); 
  } 

  @Override 
  public void setClassAssertionStatus(String className, boolean enabled) { 
    parent.setClassAssertionStatus(className, enabled); 
  } 

  @Override 
  public void clearAssertionStatus() { 
    parent.clearAssertionStatus(); 
  } 
}

Usage:

public static ClassLoader cachingClassLoader = new MyClassLoader(FXMLLoader.getDefaultClassLoader()); 

FXMLLoader loader = new FXMLLoader(resource); 
loader.setClassLoader(cachingClassLoader); 

This significantly speeds up the performance. However, there is no workaround for step 2, so this might still be a problem.

However, there are already feature requests in the official JavaFX jira for this. It would be nice of you to support this requests.

Links:

Incrustation answered 31/7, 2012 at 7:33 Comment(3)
The links in the end are no longer valid. Does anyone know what they used to point to, and can find the updated JDK-??? ID corresponding to them?Incogitant
@Itai, I have updated the dead links. You search for the bug ID on the new bug tracker at https://bugs.openjdk.java.net/. See FibreFoX's answer to "How to find bugs in new JavaFx bug tracker": https://mcmap.net/q/1176579/-how-to-find-bugs-in-new-javafx-bug-tracker/33963599#33963599Hawthorne
I was able to achieve some performance increase by using aalto-xml, a faster pure-java XML parser, but this kind of change is really digging deep to solve a fundamental architectural problem: I'm mis-using FXML. I would not recommend using FXML loaders as templates.Coating
R
3

I have had a similar issue. I also had to load a custom fxml-based component several times, dynamically, and it was taking too long. The FXMLLoader.load method call was expensive, in my case.

My approach was to parallelize the component instantiation and it solved the problem.

Considering the example posted on the question, the controller method with multithread approach would be:

private void textChange(KeyEvent event) {
    GridPane g = new GridPane();
    // creates a thread pool with 10 threads
    ExecutorService threadPool = Executors.newFixedThreadPool(10); 
    final List<Celli> listOfComponents = Collections.synchronizedList(new ArrayList<Celli>(100));

    for (int i = 0; i < 100; i++) {
        // parallelizes component loading
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                listOfComponents.add(new Celli());
            }
        });
    }

    // waits until all threads completion
    try {
        threadPool.shutdown();      
        threadPool.awaitTermination(3, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        // seems to be a improbable exception, but we have to deal with it
        e.printStackTrace();
    }

    g.getChildren().addAll(listOfComponents);
}
Roadway answered 25/7, 2013 at 15:54 Comment(1)
Which version of JavaFx did you use 2.2 or 8.O? With JavaFX8.0, your method fails with exception : "java.lang.IllegalStateException: Not on FX application thread ..."Exsiccate
H
0

Just adding code for "caching of already loaded classes" in @Sebastian sir given code. It is working for me. Please suggest changes in it for better performance.

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    System.out.println("In Class loader");

    Class result;
    System.out.println(" >>>>>> Load class : "+name);
    result = (Class)classes.get(name);
    if(result != null){
        System.out.println(" >>>>>> returning cached class.");
        return result;
    }else{
    Class<?> c = findClass(name);
    if ( c == null ) {
      throw new ClassNotFoundException( name );
    }
    System.out.println(" >>>>>> loading new class for first time only");
    return c;
    }
}
Hypoglossal answered 20/1, 2018 at 14:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.