JavaFX Optimized Asynchronous Lazy Loading of TreeItems on TreeView

JavaFX Optimized Asynchronous Lazy Loading of TreeItems on TreeView



I have an app where i have a TreeView which will have TreeItems holding a large number of leaf TreeItems. Having a huge number of TreeItems in the treeview hurts the performance of the app noticeably, to avoid that, what i will do, is i will allow only one non-leaf TreeItem to be expanded at a time, and once a TreeItem is folded, i will clear it's children, and load them asynchronously once needed (When the user expands the TreeItem).



The weird issue is, in this test below, when i first click the expand arrow on the treeitem, the children load fine, and if i fold it (which will clear children) and unfold it again, sometimes it works and others the program hogs and starts consuming 30% of the cpu for a couple of minutes then gets back running. What's weirder is that if i double click on the TreeItem to expand it (Not using the arrow) the hog starts right away, even at first program launch.


program hogs and starts consuming 30% of the cpu for a couple of minutes



What could i be possibly doing wrong here?



PS:



Some of the code in the LazyTreeItem class is inspired by James_D's answer Here



I tried running the loadItems task on the fx thread(Not using the ItemLoader), but it didn't make any difference.



Same issue occurs using both JAVA 8 and JAVA 9



App.java


public class App extends Application

private TreeView<Item> treeView = new TreeView<>();

@Override
public void start(Stage primaryStage) throws Exception
primaryStage.setTitle("TreeView Lazy Load");
primaryStage.setScene(new Scene(new StackPane(treeView), 300, 275));
initTreeView();
primaryStage.show();


private void initTreeView()
treeView.setShowRoot(false);
treeView.setRoot(new TreeItem<>(null));

List<SingleItem> items = new ArrayList<>();
for (int i = 0; i < 100000; i++)
items.add(new SingleItem(String.valueOf(i)));

TreeItem<Item> parentItem = new TreeItem<>(new Item());
parentItem.getChildren().add(new LazyTreeItem(new MultipleItem(items)));

treeView.getRoot().getChildren().add(parentItem);


public static void main(String args)
launch(args);




LazyTreeItem.java


public class LazyTreeItem extends TreeItem<Item>
private boolean childrenLoaded = false;
private boolean isLoadingItems = false;

public LazyTreeItem(Item value)
super(value);
// Unload data on folding to reduce memory
expandedProperty().addListener((observable, oldValue, newValue) ->
if (!newValue)
flush();

);


@Override
public ObservableList<TreeItem<Item>> getChildren()
if (childrenLoaded

public void loadItems()
Task<List<TreeItem<Item>>> task = new Task<List<TreeItem<Item>>>()
@Override
protected List<TreeItem<Item>> call()
isLoadingItems = true;
List<SingleItem> downloadSet = ((MultipleItem) LazyTreeItem.this.getValue()).getEntries();
List<TreeItem<Item>> treeNodes = new ArrayList<>();
for (SingleItem download : downloadSet)
treeNodes.add(new TreeItem<>(download));

return treeNodes;

;
task.setOnSucceeded(e ->
Platform.runLater(() ->
super.getChildren().clear();
super.getChildren().addAll(task.getValue());
childrenLoaded = true;
isLoadingItems = false;
);
);
ItemLoader.getSingleton().load(task);


private void flush()
childrenLoaded = false;
super.getChildren().clear();


@Override
public boolean isLeaf()
if (childrenLoaded)
return getChildren().isEmpty();

return false;




ItemLoader.java


public class ItemLoader implements Runnable
private static ItemLoader instance;
private List<Task> queue = new ArrayList<>();
private Task prevTask = null;

private ItemLoader()
Thread runner = new Thread(this);
runner.setName("ItemLoader thread");
runner.setDaemon(true);
runner.start();


public static ItemLoader getSingleton()
if (instance == null)
instance = new ItemLoader();

return instance;


public <T> void load(Task task)
if (queue.size() < 1)
queue.add(task);



@Override
public void run()
while (true)
try
Thread.sleep(500);
catch (InterruptedException e)
e.printStackTrace();


if (!queue.isEmpty())
Task task = queue.get(0);
if (task != prevTask)
prevTask = task;
task.run();
queue.remove(task);







Model (Item.java, SingleItem.java, MultipleItem.java)


public class Item


/****************************************************************
********** SingleItem ************
****************************************************************/
public class SingleItem extends Item
private String id;

public SingleItem(String id)
this.id = id;


public void setId(String id)
this.id = id;


/****************************************************************
********** MultipleItem ************
****************************************************************/
public class MultipleItem extends Item

private List<SingleItem> entries = new ArrayList<>();

public MultipleItem(List<SingleItem> entries)
this.entries = entries;


public List<SingleItem> getEntries()
return entries;


public void setEntries(List<SingleItem> entries)
this.entries = entries;







I can reproduce the issue in fx 10 (when double clicking on the item) - not when expanding on the triangle. No idea what happens, but: the leaf/leafProperty is not synched (due to the api bug, as James noted), so some internals - which are dirty anyway - might get into further trouble. In your shoes I would go into a profiling round: find the bottleneck with a profiler and then go from there.

– kleopatra
Sep 17 '18 at 9:25






ookay .. seems to be related to selection: if the expanding item is not selected, all is fine - if it is selected it hangs ...

– kleopatra
Sep 17 '18 at 11:40






it's hanging in TreeViewFocusModel which is buggy in checking the row of each of the added items (vs. querying the row of the last added in a block as TreeViewSelectionModel does) - no idea how to hack around, implementing a custom FocusModel might be a (nasty .. all is very hidden and very hard to extend) the only way out

– kleopatra
Sep 17 '18 at 12:43







definitely a bug (will file it soon), and definitely (modulo missing tests ;) can be hacked by a custom FocusModel that queries the newly added items only if focusedIndex > sourceRow (c&p the default - which is diiirty by tweaking internal state of both treeItem and treeView ... which in turn requires dirty reflective access) and then do better than default impl, no reason to query each added item for its row, doing so for the last of the added bunch should be just fine

– kleopatra
Sep 18 '18 at 10:31







@kleopatra If/when you file the bug you should include the TreeTableViewFocusModel because it does pretty much the same thing.

– Slaw
Sep 18 '18 at 11:04


TreeTableViewFocusModel




1 Answer
1



The issue, as pointed out by @kleopatra, is caused by adding a large amount of children when there are one or more items selected. One way to fix this is to try and implement your own FocusModel, as the default FocusModel seems to be the source of the problem. Another, and in my opinion easier, way to create a workaround is to clear the selection before adding the large group of children; afterwards, you can re-select the items that were previously selected.


FocusModel


FocusModel



The way I went about doing this is by firing TreeModificationEvents with custom EventTypes. Also, I decided not to override isLeaf() inside my lazy TreeItem. I find it easier to use a placeholder TreeItem for when the parent TreeItem is a lazy branch. Since there is a placeholder the parent will register as a branch automatically.


TreeModificationEvent


EventType


isLeaf()


TreeItem


TreeItem


TreeItem



Here's an example that browses the default FileSystem. To test if the solution worked I created a 100,000 file directory and opened it; there was no hang for me. Hopefully that means this can be adapted to your code.


FileSystem



Note: This example does remove the children when the branch is collapsed, just like you are doing in your code.



App.java


import java.nio.file.FileSystems;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.stage.Stage;

public class App extends Application

private static String pathToString(Path p)
if (p == null)
return "null";
else if (p.getFileName() == null)
return p.toString();

return p.getFileName().toString();


@Override
public void start(Stage primaryStage)
TreeView<Path> tree = new TreeView<>(new TreeItem<>());
tree.setShowRoot(false);
tree.setCellFactory(LazyTreeCell.forTreeView("Loading...", App::pathToString));
TreeViewUtils.installSelectionBugWorkaround(tree);

for (Path fsRoot : FileSystems.getDefault().getRootDirectories())
tree.getRoot().getChildren().add(new LoadingTreeItem<>(fsRoot, new DirectoryLoader(fsRoot)));


primaryStage.setScene(new Scene(tree, 800, 600));
primaryStage.show();





DirectoryLoader.java


import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import javafx.scene.control.TreeItem;

public class DirectoryLoader implements Callable<List<? extends TreeItem<Path>>>

private static final Comparator<Path> COMPARATOR = (left, right) ->
boolean leftIsDir = Files.isDirectory(left);
if (leftIsDir ^ Files.isDirectory(right))
return leftIsDir ? -1 : 1;

return left.compareTo(right);
;

private final Path directory;

public DirectoryLoader(Path directory)
this.directory = directory;


@Override
public List<? extends TreeItem<Path>> call() throws Exception
try (Stream<Path> stream = Files.list(directory))
return stream.sorted(COMPARATOR)
.map(this::toTreeItem)
.collect(Collectors.toList());



private TreeItem<Path> toTreeItem(Path path)
return Files.isDirectory(path)
? new LoadingTreeItem<>(path, new DirectoryLoader(path))
: new TreeItem<>(path);





LoadingTreeItem.java


import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.TreeItem;

public class LoadingTreeItem<T> extends TreeItem<T>

private static final EventType<?> PRE_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "PRE_ADD_LOADED_CHILDREN");
private static final EventType<?> POST_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "POST_ADD_LOADED_CHILDREN");

@SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> preAddLoadedChildrenEvent()
return (EventType<TreeModificationEvent<T>>) PRE_ADD_LOADED_CHILDREN;


@SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> postAddLoadedChildrenEvent()
return (EventType<TreeModificationEvent<T>>) POST_ADD_LOADED_CHILDREN;


private final Callable<List<? extends TreeItem<T>>> callable;
private boolean needToLoadData = true;

private CompletableFuture<?> future;

public LoadingTreeItem(T value, Callable<List<? extends TreeItem<T>>> callable)
super(value);
this.callable = callable;
super.getChildren().add(new TreeItem<>());
addExpandedListener();


@SuppressWarnings("unchecked")
private void addExpandedListener()
expandedProperty().addListener((observable, oldValue, newValue) ->
if (!newValue)
needToLoadData = true;
if (future != null)
future.cancel(true);

super.getChildren().setAll(new TreeItem<>());

);


@Override
public ObservableList<TreeItem<T>> getChildren()
if (needToLoadData)
needToLoadData = false;
future = CompletableFuture.supplyAsync(new CallableToSupplierAdapter<>(callable))
.whenCompleteAsync(this::handleAsyncLoadComplete, Platform::runLater);

return super.getChildren();


private void handleAsyncLoadComplete(List<? extends TreeItem<T>> result, Throwable th)
if (th != null)
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), th);
else
Event.fireEvent(this, new TreeModificationEvent<>(preAddLoadedChildrenEvent(), this));
super.getChildren().setAll(result);
Event.fireEvent(this, new TreeModificationEvent<>(postAddLoadedChildrenEvent(), this));

future = null;


private static class CallableToSupplierAdapter<T> implements Supplier<T>

private final Callable<T> callable;

private CallableToSupplierAdapter(Callable<T> callable)
this.callable = callable;


@Override
public T get()
try
return callable.call();
catch (Exception ex)
throw new CompletionException(ex);








LazyTreeCell.java


import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.util.Callback;

public class LazyTreeCell<T> extends TreeCell<T>

public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(String placeholderText,
Callback<? super T, String> toStringCallback)
return tree -> new LazyTreeCell<>(placeholderText, toStringCallback);


private final String placeholderText;
private final Callback<? super T, String> toStringCallback;

public LazyTreeCell(String placeholderText, Callback<? super T, String> toStringCallback)
this.placeholderText = placeholderText;
this.toStringCallback = toStringCallback;


/*
* Assumes that if "item" is null **and** the parent TreeItem is an instance of
* LoadingTreeItem that this is a "placeholder" cell.
*/
@Override
protected void updateItem(T item, boolean empty)
super.updateItem(item, empty);
if (empty)
setText(null);
setGraphic(null);
else if (item == null && getTreeItem().getParent() instanceof LoadingTreeItem)
setText(placeholderText);
else
setText(toStringCallback.call(item));






TreeViewUtils.java


import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.scene.control.TreeView;

public class TreeViewUtils

public static <T> void installSelectionBugWorkaround(TreeView<T> tree)
List<TreeItem<T>> selected = new ArrayList<>(0);
EventHandler<TreeModificationEvent<T>> preAdd = event ->
event.consume();
selected.addAll(tree.getSelectionModel().getSelectedItems());
tree.getSelectionModel().clearSelection();
;
EventHandler<TreeModificationEvent<T>> postAdd = event ->
event.consume();
selected.forEach(tree.getSelectionModel()::select);
selected.clear();
;
ChangeListener<TreeItem<T>> rootListener = (observable, oldValue, newValue) ->
if (oldValue != null)
oldValue.removeEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
oldValue.removeEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);

if (newValue != null)
newValue.addEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
newValue.addEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);

;
rootListener.changed(tree.rootProperty(), null, tree.getRoot());
tree.rootProperty().addListener(rootListener);


private TreeViewUtils()



As implemented, the utility method that installs the workaround is tied to you using LoadingTreeItems in the TreeView. I couldn't think of a good way to make the solution general enough to apply to any arbitrary TreeView; for that, I believe creating a custom FocusModel would be necessary.


LoadingTreeItem


TreeView


TreeView


FocusModel



There is probably a better way to implement the LazyTreeCell by using a class to wrap the real data—like what you're doing with Item. Then you could have an actual placehoder Item instance that tells the TreeCell that it is a placeholder rather than relying on what type the parent TreeItem is. As it is, my implementation is likely brittle.


LazyTreeCell


Item


Item


TreeCell


TreeItem






Thanks for taking the time to write this answer slaw. Sorry, i forgot to mention that the hang only happens when there's a huge amount of treeitems to be loaded, in my case 20,000 and above. I removed the isLeaf method as a whole but it didn't solve it. Does your solution work when a directory has 100,000 files?

– DarkMental
Sep 17 '18 at 23:18






I will try and do the test now.

– DarkMental
Sep 17 '18 at 23:20






I added this inside the else statement in directory loader int count = 0; while (++count < 100000) list.add(TreeData.createLeaf(fo)); , and expanded a drive with only one leaf file and the same hang occurred, @Kleopatra in the replies above found out it had to do with the item being selected/focused, in your example app the first treecell is always focused as indicated by the blue border, and therefore produces the hang immediately once treeitem is expanded.

– DarkMental
Sep 17 '18 at 23:53



int count = 0; while (++count < 100000) list.add(TreeData.createLeaf(fo));






arrgghh ... adding the cell into the mix is .. evil ... anyway, the issue is not related to lazy loading, happens whenever many children are added

– kleopatra
Sep 18 '18 at 12:05







@DarkMental No real advantage. Using a Task could be considered the "more JavaFX way". The reasons I used CompletableFuture are (1) it seemed simpler for a small example and (2) I'm not showing visual progress. And no, calling cancel doesn't stop the execution immediately. In fact, CompletableFuture.cancel behaves differently than a "normal" Future; it doesn't interrupt the Thread. Even if it did, my code doesn't check for interruption so it wouldn't stop execution anyway.

– Slaw
Sep 18 '18 at 20:20



Task


CompletableFuture


cancel


CompletableFuture.cancel


Future


Thread



Thanks for contributing an answer to Stack Overflow!



But avoid



To learn more, see our tips on writing great answers.



Required, but never shown



Required, but never shown




By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

How do I collapse sections of code in Visual Studio Code for Windows?

ャフサォクコ ケウ,コ,ワ メ,ロスョノ゙,クネ,フムカヤヲニ,エコ゚ツ ウイオン゙ケワサネォキモュキォウイノンコチ゚メヌナイゥフュ,カヒウネェ ネ,ホノケ,ムュキ ッボーミュハ,チ ツス ィ メウイマヤ,゙ウチ ヅ ロ,ォジヌェ ャヌット ェ,マャ,チナエヒネソキツテ トホヲヲミーァ