1

Let's say I want to load an FXML document to be used somewhere in my application. As far as I'm aware, there are two ways of doing this:

  1. Call the static FXMLLoader#load(<various resource args>) method.
  2. Initialize an FXMLLoader (with the resource location), and then call load() on that instance.

My question is what exactly "loading" an FXML document does here.

Initially, I assumed the static method would do an entire parse "cycle" on every call, and that creating an instance would allow multiple loads to take advantage of some kind of preprocessed representation, but documentation for the non-static load() method just states;

"Loads an object hierarchy from an FXML document. The location from which the document will be loaded...", which sounds like the document is loaded on every call.

I'm using JavaFX 17.

the4naves
  • 333
  • 2
  • 9
  • This is [a reasonable guide and explanation](https://edencoding.com/fxmlloader/). If you really want to understand it in detail look at the [source](https://github.com/openjdk/jfx/blob/master/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java) it is just one class albeit a good-sized, clever one. – jewelsea Sep 18 '21 at 23:24
  • 1
    James put together an [abbreviated step-wise summary of the load operation](https://stackoverflow.com/a/69081174/1155209). – jewelsea Sep 18 '21 at 23:32
  • 1
    @jewelsea Thanks for the help! From a quick look at the source as suggested, it seems that both methods always reparse everything. All overloads of `load()`, static or otherwise simply return a call to `loadImpl()`. All overloads of `loadImpl()` return a call to a different overload (sometimes with some slight processing), the chain ending in an overload which actually parses the document, which doesn't seem to check if the document was parsed previously, or anything similar. – the4naves Sep 18 '21 at 23:41
  • 1
    There is some related info here on [fxml load performance optimization](https://stackoverflow.com/questions/24077524/is-there-a-way-to-compile-load-fxml-files-faster-and-only-one-time-and-not-at). You could look at my answer and the subsequent user comments. I think my answer is both a bit outdated and probably also a bit wrong. The user comments would indicate that there is a level of caching or at least residual data in the loader instance when the instance load method is used, which would presumably not be there for the static call, though not sure if that info helps you. – jewelsea Sep 18 '21 at 23:42
  • 1
    @jewelsea Interesting, the link to an FXML compiler was especially helpful, as something like that should massively improve performance (at least relatively). I think Michel's comment about the cached values not actually accelerating further loads is correct though; `clearImports()` is always called in the final `loadImpl()` overload, which in turn clears two lists used to cache lookup values. `packages`, for example, is used to look up classes in the `getType()` method, which is in turn used by various `process()` methods used in the XML parsing section. – the4naves Sep 19 '21 at 00:06
  • @jewelsea Additionally, the FXML data itself is just loaded by an `InputStreamReader` which is used to create an `XMLStreamReader` using a factory, which itself is newly initialized for each parse. The only other things I see maybe being used in some sort of cache are a couple `LinkedList`'s in the abstract `Element` class, which *does* contain the aforementioned `process()` methods (which also access those lists), so it's possible something there is kept across loads, but I haven't looked enough to be sure. – the4naves Sep 19 '21 at 00:24
  • Looking a bit farther, the only place I see `Element` (or more specifically subclasses of `Element`) being stored is in a variable named `current`, used by the `processStartElement()` method. New initializations of `current` never seem to use the old value, so I think those lists are destroyed too. – the4naves Sep 19 '21 at 00:33
  • You could probably take all that info and remove it from the comments and put it into a self-answer if you wished. I think it would be ok to copy the relevant bits from James answer with credit and includdd if there too, then there would be a pretty good summary of how the loader works, which would be neat. – jewelsea Sep 19 '21 at 02:08
  • @jewelsea I can probably do that. I'm a little nervous that I've missed something important, so I might spend a bit more time looking over the source, but if I can't find anything obvious by tomorrow I'll bite the bullet and write out an answer. – the4naves Sep 19 '21 at 02:27

1 Answers1

2

After spending a fair bit of time with the source, I feel I can give a pretty good overview of how FXML loading functions behind the scenes. That being said, I can't guarantee that there isn't anything I didn't miss. I've thoroughly looked over quite a bit of code I thought to be important, but most isn't all, and I may have simply not noticed something.

This answer should be valid for JavaFX 17.

As a TLDR answering the main concern of my question: As far as I can tell, no information is cached across load() calls, regardless of whether you use the static or non-static versions. That being said, the non-static calls will still give you a slight performance gain, the fastest of which is the load(InputStream inputStream) overload, which (in addition to skipping some argument processing) will prevent the loader from opening a new InputStream on every call.

I've built a call graph (CallGraph Viewer) showing important parts of the FXML loading code in order to make it a bit more digestible. This is easily the most likely part of my answer to contain inaccuracies. To generate this graph, I simply copied the FXMLLoader code into eclipse and generated connections for parts of the code I deemed important. Unfortunately, the plugin doesn't always correctly parse code containing missing imports, requiring me to write in definitions for a couple of classes, but I left most alone. Additionally, the initial result was incomprehensible and needed a fair bit of manual cleanup, a large portion of which was done simply based on whether I thought something sounded useful or not.

If you are unfamiliar with eclipse's icons, documentation can be found here (make sure to zoom the image, or open it in a new tab, or I doubt you will be able to see much).
Yes, there are three processEndElement() methods with the same signature, they are overridden methods in subclasses of Element. FXMLLoader call graph If you're wondering what I spent all that manual cleanup time on, try not to worry about the individual methods, more the overall structure.

Here's my breakdown of this mess as a step by step recreation of what happens when load() is called:

  1. The application calls one of the public load() methods. This simply calls a matching loadImpl() overload (static if the load() call was static and vice-versa) with the provided arguments. All existing loadImpl() overloads also ask for the class which called them, which the method attempts to provide with a java.lang.StackWalker. No additional processing is done.

  2. After passing the public interface, execution is routed through a hierarchy of loadImpl() calls. Each overload just calls an overload with one more argument than itself, passing on its own arguments and giving null for the missing one (except in the case of a missing charset, which is given a default value).
    The more arguments you give to load(), the farther you start in the hierarchy, with non-static versions beginning after the static ones. If you call one of the static overloads, an instance of the FXMLLoader class is created at the final static loadImpl(), which is used to continue onto the non-static calls.

  3. Once reaching the non-static loadImpl() calls, things begin to get interesting. If using the load(void) overload, an InputStream is created based on arguments set when the FXMLLoader instance was initialized, and is given to the next stage in the hierarchy as before. At the final (non-static) loadImpl() (which can be called immediately using the load(InputStream inputStream) overload; this is the fastest method I know of to get from the initial load() call to XML processing), we finally exit the loadImpl() hierarchy, and move to XML processing.

  4. Two things happen here:

    1. a ControllerAccessor instance is given the callingClass argument passed up the loadImpl() hierarchy. I can't exactly explain how this class works, but it contains two Map's; controllerFields and controllerMethods, used in the initialization of controllers.
    2. clearImports() is called, clearing packages (a List) and classes (a Map), both used in further XML processing.

    The four variables here (except for maybe the controller ones, I'm a little iffy on them) act as important cache data for the backend XML processing cycle. However, all are cleared between loads (there is no logic controlling their execution, if the load succeeded, the cache data will not have survived), so using an FXMLLoader instance will not improve performance due to data caching (it's still worth using one, however, as the non-static calls skip much of the loadImpl() hierarchy, and you can even reuse the InputStream if using that particular overload).

  5. Next, the XML parser itself is loaded. First, a new instance of a XMLInputFactory is created. This is then used to create a XmlStreamReader from the provided InputStream

    Finally, we now begin actually processing the loaded XML.

  6. The main XML processing loop is actually relatively simple to explain;
    First, the code enters a while loop, checking the value of xmlStreamReader.hasNext().

    During each cycle, a switch statement is entered, routing execution to different process<X>() methods depending on what the XML reader encounters. Those methods process the incoming events, and use an assortment of more "backend" methods to carry out common operations (The 'backend XML processing' section of the call graph is only a small portion of the actual code). These include methods like processImports(), which calls importPackage() or importClass(), in turn populating the packages and classes caches. Those caches are accessed by getType(), a backend method used by many other processing methods.

    Additionally, I think that some part of controllers is "assigned" during this stage; processEndElements(), for example, ends up calling getControllerFields() or getControllerMethods(), which access the aforementioned controllerFields and controllerMethods caches, but also sometimes modify them. That being said, the call graph gets a bit too deep for me to easily understand at this point, and those methods are also called later, so I can't be sure.

  7. After XML processing, a controller (controllers? see comment below) is initialized. You can read about controller initialization a bit in James_D's answer here, but I don't have much to say about it, as this is the section I am least confident in understanding.

    That being said, it is interesting to note that this code is out of the previous while loop; only one initialization method is called. Either what seems like one call is actually multiple (which is definitely possible; the initialization "method" called is returned by controllerAccessor.getControllerMethods() and "it" is called using the MethodHelper JavaFX class), or only one controller is initialized here (assumedly the controller for the root node) and the others are initialized during parsing. I'd lean towards the first possibility here, but that's based purely on intuition.

  8. Finally (and if you're still reading by now, consider me impressed), we enter cleanup. This stage is super simple;

    1. The ControllerAccessor has its "calling class" variable nulled, and its controllerFields and controllerMethods caches cleared.
    2. The XmlStreamReader instance is nulled.
    3. The root node is returned, and thus the function exits.

Thanks to @jewelsea for links to other answers and for recommending I look at the source.

the4naves
  • 333
  • 2
  • 9