1

There are several related questions, about auto-expanding a JTree when a new TreeModel is set, or about expanding a JTree in general, and some of them are also aiming at the performance of expanding many paths in a JTree.

However, none of the proposed solutions seems to cover what one could consider a "simple" application case: I have a large tree (that is, a tree that is either very deep, very broad, or both), and I want to fully expand it programmatically.

The following is a MCVE that shows the problem: It creates a tree model with 100k nodes. Pressing the button triggers a call to expandAll, which tries to expand all nodes using an approach that was derived from the answers to the related questions.

The problem is that expanding these 100k nodes takes approximately 13 seconds (on an average machine, with a recent JVM).

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.util.function.Consumer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;


public class TreeExpansionPerformanceProblem
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(
            () -> createAndShowGUI());
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(1,0));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceProblem::expandAll));

        f.setSize(800,600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
    {
        JPanel panel = new JPanel(new BorderLayout());
        JTree tree = new JTree(createTestTreeModel());
        panel.add(new JScrollPane(tree), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        JButton expandAllButton = new JButton("Expand all");
        expandAllButton.addActionListener( e -> 
        {
            System.out.println("Expanding...");
            long before = System.nanoTime();
            expansionMethod.accept(tree);
            long after = System.nanoTime();
            System.out.println("Expanding took "+(after-before)/1e6);

        });
        buttonPanel.add(expandAllButton);
        panel.add(buttonPanel, BorderLayout.SOUTH);
        return panel;
    }

    private static void expandAll(JTree tree)
    {
        int r = 0;
        while (r < tree.getRowCount())
        {
            tree.expandRow(r);
            r++;
        }
    }

    private static TreeModel createTestTreeModel() 
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
        addNodes(root, 0, 6, 6, 10);
        return new DefaultTreeModel(root);
    }

    private static void addNodes(DefaultMutableTreeNode node, 
        int depth, int maxDepth, int count, int leafCount)
    {
        if (depth == maxDepth)
        {
            return;
        }
        for (int i=0; i<leafCount; i++)
        {
            DefaultMutableTreeNode leaf = 
                new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
            node.add(leaf);
        }
        if (depth < maxDepth - 1)
        {
            for (int i=0; i<count; i++)
            {
                DefaultMutableTreeNode child = 
                    new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                node.add(child);
                addNodes(child, depth+1, maxDepth, count, leafCount);
            }
        }

    }
}

Are there any options that allow expanding many nodes more efficiently?

Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159

2 Answers2

7

There are various bottlenecks when fully expanding a large tree, and different ways to circumvent these.

Interestingly, collecting the TreePath objects for the expansion and traversing the tree in general is not the most expensive part. According to profiler runs in the VisualVM and in the Java Flight Recorder, most of the time is spent when computing the "mapping" between the model state (the TreeModel) and the view (the JTree). This mainly refers to

  • computing the row heights for the JTree
  • computing the bounds of the labels in the TreeCellRenderer

Not all of these computations may be avoided. However, expanding the tree can be made significantly faster by

  • setting a fixed row height, with JTree#setRowHeight
  • temporarily disabling the TreeExpansionListeners of the tree

The following is an MCVE that compares the "naïve" approach from the question, which takes 13 seconds for expanding a tree with 100k nodes, to a slightly faster approach, that only takes 1 second for expanding the same tree.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;


public class TreeExpansionPerformanceSolution
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(
            () -> createAndShowGUI());
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(1,0));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceSolution::expandAll));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceSolution::expandAllFaster));

        f.setSize(800,600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
    {
        JPanel panel = new JPanel(new BorderLayout());
        JTree tree = new JTree(createTestTreeModel());
        panel.add(new JScrollPane(tree), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        JButton expandAllButton = new JButton("Expand all");
        expandAllButton.addActionListener( e -> 
        {
            System.out.println("Expanding...");
            long before = System.nanoTime();
            expansionMethod.accept(tree);
            long after = System.nanoTime();
            System.out.println("Expanding took "+(after-before)/1e6);

        });
        buttonPanel.add(expandAllButton);
        panel.add(buttonPanel, BorderLayout.SOUTH);
        return panel;
    }

    private static void expandAll(JTree tree)
    {
        int r = 0;
        while (r < tree.getRowCount())
        {
            tree.expandRow(r);
            r++;
        }
    }

    private static void expandAllFaster(JTree tree)
    {
        // Determine a suitable row height for the tree, based on the 
        // size of the component that is used for rendering the root 
        TreeCellRenderer cellRenderer = tree.getCellRenderer();
        Component treeCellRendererComponent = 
            cellRenderer.getTreeCellRendererComponent(
                tree, tree.getModel().getRoot(), false, false, false, 1, false);
        int rowHeight = treeCellRendererComponent.getPreferredSize().height + 2;
        tree.setRowHeight(rowHeight);

        // Temporarily remove all listeners that would otherwise
        // be flooded with TreeExpansionEvents
        List<TreeExpansionListener> expansionListeners =
            Arrays.asList(tree.getTreeExpansionListeners());
        for (TreeExpansionListener expansionListener : expansionListeners)
        {
            tree.removeTreeExpansionListener(expansionListener);
        }

        // Recursively expand all nodes of the tree
        TreePath rootPath = new TreePath(tree.getModel().getRoot());
        expandAllRecursively(tree, rootPath);

        // Restore the listeners that the tree originally had
        for (TreeExpansionListener expansionListener : expansionListeners)
        {
            tree.addTreeExpansionListener(expansionListener);
        }

        // Trigger an update for the TreeExpansionListeners
        tree.collapsePath(rootPath);
        tree.expandPath(rootPath);
    }

    // Recursively expand the given path and its child paths in the given tree
    private static void expandAllRecursively(JTree tree, TreePath treePath)
    {
        TreeModel model = tree.getModel();
        Object lastPathComponent = treePath.getLastPathComponent();
        int childCount = model.getChildCount(lastPathComponent);
        if (childCount == 0)
        {
            return;
        }
        tree.expandPath(treePath);
        for (int i=0; i<childCount; i++)
        {
            Object child = model.getChild(lastPathComponent, i);
            int grandChildCount = model.getChildCount(child);
            if (grandChildCount > 0)
            {
                class LocalTreePath extends TreePath
                {
                    private static final long serialVersionUID = 0;
                    public LocalTreePath(
                        TreePath parent, Object lastPathComponent)
                    {
                        super(parent, lastPathComponent);
                    }
                }
                TreePath nextTreePath = new LocalTreePath(treePath, child);
                expandAllRecursively(tree, nextTreePath);
            }
        }
    }


    private static TreeModel createTestTreeModel() 
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
        addNodes(root, 0, 6, 6, 10);
        return new DefaultTreeModel(root);
    }

    private static void addNodes(DefaultMutableTreeNode node, 
        int depth, int maxDepth, int count, int leafCount)
    {
        if (depth == maxDepth)
        {
            return;
        }
        for (int i=0; i<leafCount; i++)
        {
            DefaultMutableTreeNode leaf = 
                new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
            node.add(leaf);
        }
        if (depth < maxDepth - 1)
        {
            for (int i=0; i<count; i++)
            {
                DefaultMutableTreeNode child = 
                    new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                node.add(child);
                addNodes(child, depth+1, maxDepth, count, leafCount);
            }
        }

    }
}

Notes:

  • This is a self-answered question, and I hope that this answer may be helpful for others. Nevertheless, 1 second is still rather slow. I tried some other things as well, e.g. setting tree.setLargeModel(true), but this did not have a positive effect (in fact, it was even slower!). Most of the time is still spent in the final update of the visual state of the tree, and I'd be happy to see further improvements here.

  • The expandAllRecursively method could be replaced by few lines involving DefaultMutableTreeNode#breadthFirstEnumeration and DefaultTreeModel#getPathToRoot, without sacrificing much of the performance. But in the current form, the code solely operates on the TreeModel interface, and should work with any kind of nodes.

Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159
  • Because the UI delegate owns the `CellRendererPane`, discussed [here](http://stackoverflow.com/a/7776211/230513), I see some variability among L&F implementations; I see better absolute performance but lower relative improvement on Mac OS X (~8:2). – trashgod Jan 19 '16 at 13:39
  • @trashgod Certainly, some variations are to be expected here. Admittedly, I did not systematically try different L&Fs, but maybe I'll extend the Q/A accordingly, this could be interesting. If you have any ideas (even guesses) of how this could be made faster, I'd also be interested to hear them. – Marco13 Jan 19 '16 at 16:10
4

As discussed here, JTree already uses the flyweight pattern to optimize rendering. I'd argue that your approach in expandAllFaster() is sufficient. Expanding all of >105 leaves is unwieldy at best. The resulting tree is difficult to browse meaningfully, although suitable search controls may mitigate this.

An interesting compromise is seen in the Mac OS X TreeUI delegate, com.apple.laf.AquaTreeUI. It recursively expands the selected node and its children when the option key is pressed, as determined by MouseEvent::isAltDown(). See the Action named "aquaFullyExpandNode" for details.

Finally, saving the user's expansion as a preference might be worthwhile, for example.

I'm working on…filtering a >100k-node-JTree on the fly.

Focusing on a model-based approach, as suggested here, move the search to a separate, perhaps modeless, dialog. In outline,

  • Construct a prefix tree based on the tree model to be used as a dictionary, perhaps using one of the approaches suggested here.

  • Let a DocumentListener monitor the search field and condition a custom TableModel to display matches as the user types.

  • Display no matches until some minimum number of characters has been typed; three is a common choice for large models.

  • Let a TableModelListener expand tree nodes corresponding to selected rows; alternatively, expand selected rows in an Expand button handler; in a modeless context, fire a suitable PropertyChangeEvent for which the tree should listen.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thanks for this hint. From a quick glance at the source code, it seems that the `aquaFullyExpandNode` action eventually ends at the [`expandAllNodes`](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/macosx/classes/com/apple/laf/AquaTreeUI.java#l559) method, which is similar to the one in the question. (to be continued...) – Marco13 Jan 19 '16 at 18:05
  • Of course, showing 100k nodes hardly makes sense, but this is exactly the "root cause" of this question: I'm working on a "filtered JTree" that allows filtering a >100k-nodes-JTree on the fly - but when the first letter is typed into the filter-textfield, it still may have to expand >100k nodes (or even *many* more - that's why I want to add this filter in the first place). (BTW: There seem to be no nice, generic, efficient model-based solutions for a filtered tree, but that's a different issue - I considered adding a bounty to https://stackoverflow.com/questions/9234297/ for this one...) – Marco13 Jan 19 '16 at 18:09
  • And, one more (sorry) regarding saving the preference: As far as I see, the main problem is that *building the visual structures* in the JTree (e.g. determining the bounds of all cells) is the expensive part - and this has to be done "in any case" (unless there are really tricky workarounds for that one). So even if it uses the flyweight pattern for the cells, it still has to compute their *bounds* in order to determine the size of the tree. And all this is hidden in the TreeUI implementations, of course... – Marco13 Jan 19 '16 at 18:12
  • @Marco13: I've outlined one approach above. – trashgod Jan 19 '16 at 21:24
  • The idea was that as long as there is a non-`null` filter, all matching nodes should be expanded - and this caused the problem. (When the filter is set to be non-`null`, the expansion state is saved, and restored when the filter becomes `null` again). Your third point may be a reasonable workaround from an application point of view, but in general, from a library point of view, one can simply not say what the particular filter does. (Context: https://github.com/javagl/CommonUI/blob/master/src/main/java/de/javagl/common/ui/tree/filtered/FilteredTree.java#L171 - just playing around there). – Marco13 Jan 19 '16 at 21:47
  • You might be able to use the strategy pattern to offer common partitions, eg. prefix for a string or regular period for a date. – trashgod Jan 20 '16 at 10:34
  • I didn't unterstand the last one, admittedly. Depending on the tree itself and the filter, the filtered tree *may* have >100k nodes, and they should all be expanded. At least, they should "appear" to be expanded: I hoped that the expensive computations of the node cell bounds could somehow be avoided or "deferred", by checking wich nodes are actually *visible* (in a scrollpane viewport, usually). But I'll have to revise the filtering in general anyhow, and maybe will also invest some more time to see how *expanding* can be made faster. – Marco13 Jan 20 '16 at 10:41
  • Sorry to be so elliptical; by partition I mean a way to focus the search on a subtree, thereby minimizing the number of nodes affected. e.g. an initial `a` eliminates >92% of the remaining nodes in English. – trashgod Jan 20 '16 at 12:07
  • I see, but just to give an example: Imagine a (large) "file tree", where ALL nodes start with `"C:\"`, and the user enters exactly `"C:\..."` into the filter text field... There certainly are ways to optimize the search process, filtering, and thus the tree size for certain application cases. But in the end, I "need" a way to expand a large tree, regardless of its contents, as quickly as possible. I'll try to examine further options for this ASAP. – Marco13 Jan 20 '16 at 12:19