JFace CheckBoxTreeViewer aktualisieren unglaublich langsam

Unten ist ein (mit SWT und JFace direkt compilier- und testbares) Beispiel für etwas, was eigentlich gar nicht so kompliziert und aufwändig sein sollte: Ein CheckboxTreeViewer, mit zwei Spalten. In einer steht einfach ein Name für die Knoten, und in der anderen ob der Knoten “gecheckt” ist oder nicht (soll nur ein Test sein).

Wenn man den Check-Status eines Knotens ändert, werden auch die Kinder des Knoten ensprechend gecheckt- und ungecheckt.

Dass schon das Erstellen (bzw. erstmaligen Ausklappen) eines Baumes mit lächerlichen 1000 Knoten mehrere Sekunden dauert, ist irritierend. Das könnte aber gerade noch so hinnehmbar sein, weil es nur einmal passieren muss, und man da notfalls wohl mit diesem Lazy Provider etwas tricksen könnte.

Was die Grenze zum Absurden aber schon deutlich überschreitet, ist, dass ein Aktualisieren der Labels bei einer Änderung des Checkbox-Status für 1000 Knoten sage und schreibe fünfeinhalb Sekunden dauert.

(Man könnte das zwar für einige Fälle optimieren, indem man die zu aktualisiernden Elemente beim Event mit übergibt - aber spätestens wenn man den Wurzelknoten klickt, ist man wieder bei den 5 Sekunden).

Da das ganze auch irgendwann mal für bis zu 1000000 Knoten flott laufen sollte, hoffe ich, dass ich da etwas sehr einfaches und naheliegendes falsch gemacht habe…

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ICheckStateProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;

public class CheckboxTreeViewerPerformanceTest
{
    public static void main(String[] args)
    {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setBounds(100, 100, 800, 600);
        shell.setLayout(new FillLayout());
        new CheckboxTreeViewerPerformanceTest(shell);
        shell.open();
        while (!shell.isDisposed())
        {
            if (!display.readAndDispatch())
            {
                display.sleep();
            }
        }
        display.dispose();
        
    }


    // A simple node containing a name and children, and a flag indicating 
    // whether the node is "checked" or not.
    // Changes in the "checked" flag will be propagated to the children.
    static class TestNode
    {
        private final String name;
        private final List<TestNode> children;
        private boolean checked;
        
        public TestNode(int id)
        {
            this.name = "node"+id;
            this.children = new ArrayList<TestNode>();
            this.checked = true; 
        }
        
        void addChild(TestNode child)
        {
            children.add(child);
        }

        String getName()
        {
            return name;
        }
        
        boolean isChecked()
        {
            return checked;
        }
        
        void setChecked(boolean checked)
        {
            this.checked = checked;
            for (TestNode child : children)
            {
                child.setChecked(checked);
            }
        }
        
        List<TestNode> getChildren()
        {
            return children;
        }
    }
    
    private static TestNode createModel()
    {
        int id = 0;
        TestNode root = new TestNode(id++);
        int n = 1000;
        for (int i=0; i<n; i++)
        {
            root.addChild(new TestNode(id++));
        }
        return root;
    }
    
    
    public CheckboxTreeViewerPerformanceTest(Composite parent)
    {
        CheckboxTreeViewer viewer = new CheckboxTreeViewer(
            parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
        viewer.setContentProvider(new TestNodeContentProvider());
        viewer.setCheckStateProvider(new TestCheckStateProvider());
        
        Tree tree = viewer.getTree();
        tree.setHeaderVisible(true);
        
        // Create one column showing the node name
        TreeViewerColumn nameColumn = new TreeViewerColumn(viewer, SWT.NONE);
        nameColumn.getColumn().setText("Name");
        nameColumn.getColumn().setWidth(200);
        nameColumn.setLabelProvider(
            new DelegatingStyledCellLabelProvider(new NameLabelProvider()));

        // Create one column showing the "checked" state of the nodes
        TreeViewerColumn checkedColumn = new TreeViewerColumn(viewer, SWT.NONE);
        checkedColumn.getColumn().setText("Checked");
        checkedColumn.getColumn().setWidth(70);
        checkedColumn.getColumn().setAlignment(SWT.RIGHT);
        CheckedLabelProvider checkedLabelProvider = new CheckedLabelProvider();
        checkedColumn.setLabelProvider(
            new DelegatingStyledCellLabelProvider(checkedLabelProvider));

        viewer.addCheckStateListener(new ICheckStateListener()
        {
            @Override
            public void checkStateChanged(CheckStateChangedEvent event)
            {
                Object element = event.getElement();
                if (element instanceof TestNode)
                {
                    TestNode node = (TestNode)element;
                    
                    // Set the checked state of the node and all its children
                    node.setChecked(event.getChecked());
                    viewer.setSubtreeChecked(element, event.getChecked());
                    
                    // Trigger an update of the "checked" labels
                    long before = System.nanoTime();
                    checkedLabelProvider.fireUpdateLabels();
                    long after = System.nanoTime();
                    System.out.println("update took "+(after-before)/1e6);
                }
            }
        });
        
        TestNode root = createModel();
        viewer.setInput(new Object[] { root });
    }
    
    // The label provider for the "checked" column
    class CheckedLabelProvider extends LabelProvider implements
        IStyledLabelProvider
    {
        void fireUpdateLabels()
        {
            fireLabelProviderChanged(
                new LabelProviderChangedEvent(this));
        }
        
        @Override
        public StyledString getStyledText(Object element)
        {
            if (element instanceof TestNode)
            {
                TestNode node = (TestNode) element;
                return new StyledString(String.valueOf(node.isChecked()));
            }
            return null;
        }
    }
    
    
    // The label provider for the "name" column
    class NameLabelProvider extends LabelProvider 
        implements IStyledLabelProvider
    {
        @Override
        public StyledString getStyledText(Object element)
        {
            if (element instanceof TestNode)
            {
                TestNode node = (TestNode) element;
                return new StyledString(node.getName());
            }
            return null;
        }
    }
    
    // The check state provider that reads the state from the model
    private final class TestCheckStateProvider implements ICheckStateProvider
    {
        @Override
        public boolean isGrayed(Object element)
        {
            return false;
        }

        @Override
        public boolean isChecked(Object element)
        {
            if (element instanceof TestNode)
            {
                TestNode node = (TestNode)element;
                return node.isChecked();
            }
            return false;
        }
    }
    
    // A simple content provider for the tree model
    class TestNodeContentProvider implements ITreeContentProvider
    {
        @Override
        public void inputChanged(Viewer v, Object oldInput, Object newInput)
        {
            // Nothing to do here
        }

        @Override
        public void dispose()
        {
            // Nothing to do here
        }

        @Override
        public Object[] getElements(Object inputElement)
        {
            return (Object[])inputElement;
        }

        @Override
        public Object[] getChildren(Object parentElement)
        {
            TestNode node = (TestNode)parentElement;
            return node.getChildren().toArray();
        }

        @Override
        public Object getParent(Object element)
        {
            return null;
        }

        @Override
        public boolean hasChildren(Object element)
        {
            TestNode node = (TestNode)element;
            return !node.getChildren().isEmpty();
        }

    }
}

Ich hab mir mal eine pom erstellt und gegen jface 3.5 bauen lassen.

Eclipse und maven ist noch nicht so das wahre, deshalb über org.mod4j

[XML]<?xml version="1.0" encoding="UTF-8"?>

4.0.0
my.company
checkboxtreeviewer
1.0-SNAPSHOT
jar

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>



maven-eclipse-repo
http://maven-eclipse.github.io/maven

<dependencies>
    
    <dependency>
        <groupId>org.eclipse.swt</groupId>
        <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
        <version>4.3</version>
    </dependency>

    <dependency>
        <groupId>org.mod4j.org.eclipse</groupId>
        <artifactId>jface</artifactId>
        <version>3.5.0</version>
    </dependency>

    <dependency>
        <groupId>org.mod4j.org.eclipse</groupId>
        <artifactId>swt</artifactId>
        <version>3.5.0</version>
    </dependency>

    <dependency>
        <groupId>org.mod4j.org.eclipse.ui</groupId>
        <artifactId>workbench</artifactId>
        <version>3.5.0</version>
    </dependency>       
    
</dependencies>

[/XML]

Das aktualisieren der Labels braucht wirklich lange, allerdings bleibt es laut Log immer noch unter einer Sekunde.

Was teilweise etwas bringt ist auf das updaten des Labelproviders zu verzichten und statt dessen die Änderung im Model zu propagieren.

for(TestNode n: node.getChildren()) {
  viewer.update(n, null);
}
// checkedLabelProvider.fireUpdateLabels();```

Damit aktualisiert man nur den Subtree. Wenn man den ganzen Baum aktualisiert, dann sieht das wieder anders aus und ist etwas langsamer. Bei diesem Model wird es etwas deutlicher.

```private static TestNode createModel() {
        int id = 0;
        TestNode root = new TestNode(id++);
        int n = 1000;
        for (int i = 0; i < n; i++) {
            TestNode child = new TestNode(id++);
            root.addChild(child);
            for (int j = 0; j < 5; j++) {
                child.addChild(new TestNode(i* 10000 + j));                
            }
        }
        return root;
    }```

Was ich mir auch noch überlegt habe ist auf StyledString und IStyledLabelProvider zu verzichten und an deren Stelle von ColumnLabelProvider zu erben und diesen dann nur den entsprechenden String zurückgeben lassen.
Bringt aber nicht wirklich was. Vermutlich läuft da im Hintergrund das selbe ab.

Hm. Also, hier (an einem älteren Rechner) habe ich das ganze jetzt mal auf 500 Knoten reduziert (weil es sonst >10 Sekunden gedauert hat). Aber mit dem ursprünglichen
checkedLabelProvider.fireUpdateLabels();
dauert es da 3.7 Sekunden, und mit dem

    viewer.update(node, null);
    for(TestNode n: node.getChildren()) {
      viewer.update(n, null);
    }

dauert es 5.4 Sekunden…

Aber ich denke, ich habe die Lösung gefunden: Ich bin (obwohl ich gestern schon eine Weile gesucht hatte, erst) jetzt über so einen Eclipse-Forum-Eintrag gestolpert:

Wenn man direkt nach dem Erstellen des Viewers
viewer.setUseHashlookup(true);
setzt, dann dauert das update (mit dem Event, aber das ist dann fast schon egal) gerade noch

14 Millisekunden!

Mit 10000 Knoten hier auf der lahmen Kiste sind’s dann 280ms, was natürlich immernoch recht langsam ist, aber schonmal etwas, worauf man aufbauen kann.

EDIT> BTW, erfreulicherweise ist auch das Erstellen des Baumes entsprechend schneller! <EDIT

Bei Gelegenheit werde ich (auf dem System, wo das ganze RCP/JFace-Zeug mit source lookup richtig eingerichtet ist - das ist wirklich ein Krampf … (was genau ist “mod4j” denn eigentlich?) nochmal durch den Code browsen, und sehen, was dieses magische Flag denn macht. Selbst wenn die standardmäßig keinen Hash-basierten lookup machen, sondern für jedes ModellElement linear suchen, um das passende ViewElement zu finden, sollte das bei 1000 Elementen ja nicht mehrere Sekunden dauern. Da muss entweder jemand bei der Implementierung etwas gnadenlos verbockt haben, oder es muss äußere Zwänge geben, die ihn zu so etwas genötigt haben … was mich etwas beunruhigen würde, weil sowas nicht nur ein “Stolperstein” sein kann, sondern eine Fallgrube, an deren Boden angespitzte Holzpfähle stehen, und es mir schwerfällt, mir vorzustellen, wie man auf sowas aufbauend effizient nachhaltige Software entwickeln soll… -_-

Bei swt, eclipse Projekten ist es immer etwas mühselig, die ganzen Abhängigkeiten zusammenzutragen. Deshalb finde ich ist so ein Maven-Build ganz pragmatisch. Das blöde ist nur, dass es im Eclipse Umfeld etwas schwierig ist da das passende Repository zu finden. mod4j scheint irgendein Plugin zu sein, die dann halt das JFace-Zeug in der modernsten Version gemirrort/veröffentlicht haben. Für swt gibt es zumindest etwas via github, aber lies selbst https://github.com/maven-eclipse/maven-eclipse.github.io Ist ganz praktisch, wenn man mal schauen möchte wie sich das in einer anderen Version verhält.

Hab aber selbst noch mal ein bisschen rumprobiert. Ein Leitfaden, JFace-For-Dummies gibt es leider nicht.

Wie sieht denn dein Anwendungsfall genau aus?

1 Rootknoten
10.000 Knoten auf Ebene 1

Dann brauchst du den Tree ja eigentlich nicht, sondern könntest mit einem CheckboxTableViewer arbeiten. Alle oder keinen Knoten Auswählen, dann über einen zusätzlichen Button.
Habe dies gerade ausprobiert, da ist die Geschwindigkeit sogar bei 1.000.000 Elementen hervorragend. Öffnen dauert initial paar Sekunden.
Beim CheckboxTableViewer habe ich zusätzlich noch ein SWT.VIRTUAL als Flag mitgegeben.
Und wenn man in der Tabelle nur noch ein Element Ändern kann, dann kann man hier auch mit einem viewer.update(element,null) (unter einer ms, anstelle von 90ms) arbeiten.

Aber zurück zum Tree.
Wenn der Tree mehrere Ebenen hat und es wird ein Blatt auf Ebene 5 angekreuzt, dann ist dies immer noch schnell. Wenn allerdings zuerst alle Elemente über den root angekreuzt werden, dann dauert das Ändern des Knotens auf Ebene 5 danach genausolange, wie das Ändern aller Knoten.

SWT.VIRTUAL kann man beim Tree auch noch versuchen. Bringt aber nicht wirklich was. Dazu braucht es wohl den LazyContentProvider.

An stelle von fireUpdateLabels, kann man unter Umständen auch ein viewer.refresh(updateLabels); aufrufen.

Es gibt aber auch die Empfehlung Alternativen wie Nebula Nattable anstelle der JFace-Varianten zu nutzen.

Ein bisschen hängt es auch noch vom Betriebssystem ab und der Implementierung, Anbindung der zur Verfügung stehenden Komponenten. Bei Trees soll Windows etwas schlechter dastehen.

Edit: Dieser Beitrag kopiert und Rest verschoben nach http://forum.byte-welt.net/java-forum/awt-swing-javafx-swt/18094-strukturierung-und-dependencies-rcp-mit-swt-und-jface.html

Hmja, es war schon etwas irritierend, dass bei einer Suche nach “jface download” praktisch keine Ergebnisse kommen (zumindest keine, die über “Such’ dir den Sche!ß halt selbst zusammen” hinausgehen :rolleyes: ). Zum Glück gibt es ja sowohl für SWT als auch JFace viele “Snippets”, wo man schonmal was zum Ausprobieren hat. Aber die Erklärungen kommen da (im Vergleich zu den ausführlichen “How to use…”-Seiten für die Swing Components) schon etwas zu kurz.

Insgesamt mache ich gerade die ersten Experimente mit SWT und JFace. Die Anforderungen sind (Projektseitig) noch nicht ganz klar (und wahrscheinlich werden sie wieder mal nach der Deadline geklärt, durch Beschwerden “Dies-und-das hätte aber so-und-so sein sollen!” :rolleyes: ). Aber der Baum wird wohl schon notwendig sein. Dieses “1 root, 10000 blätter” war für den Test. Im Extremfall könnte er zwar recht flach mit “mehreren hunderttausend” Blättern sein, aber der relevante(ste) Fall wäre wohl was mit ~5-10 Ebenen und ~10000 Blättern.

Mit den SWT.VIRTUAL und LazyContentProvider hatte ich auch rumprobiert (nachdem es eben erst so langsam war, waren das die Richtungen, in die die ersten Suchergebnisse gingen). Aber das schien dann schon deutlich komplizierter… gerade der LazyContentProvider wäre dann was, wo man sich etwas mehr Zeit zum Reinfräsen nehmen müßte, und soweit ich das sehe, wäre es da notwendig, dass die Knoten ihre parents kennen müßten - was nur ganz schlecht mit dem Datenmodell vereinbar wäre.

Welche Alternativen für die updates es da gibt, … ja, ich hatte ein bißchen rumgesucht und rumprobiert, und auch mal im Profiler geschaut, wo da die Zeit flöten geht, aber … die verschwindet eben irgendwo den den Tiefen von (geschätzt) >100-Zeiligen Aufrufbäumen, wo die Beantwortung der Fragen

  1. Was macht der da?
  2. Warum macht er das?
  3. Warum dauert das so lange?
    nicht in vertretbarer Zeit möglich wäre. Und der Anwendungsfall (ein Baum mit 1000 Knoten) und die Aufgabe (Beschriftung der Knoten ändern) erschienen mir SO alltäglich, und die benötigte Zeit (ich meine, 5 Sekunden!? Haaaalllooo?) erschienen mir SO lächerlich-absurd hoch, dass ich davon ausgehen mußte, da etwas grundsätzlich falsch zu machen.

(Inzwischen finde ich, dass nicht ICH etwas falsch gemacht habe, sondern der, der meinte, “setUseHashlookup(true)” nicht zum default zu machen, aber … man muss sich wohl damit abfinden… )

Dieses “Nebula” sieht schonmal ganz interessant aus. Ich werde da mal ein Auge drauf behalten, auch wenn ich im Moment davon ausgehe, dass ich das nicht verwenden darf. (In der Leistungsbeschreibung steht sinngemäß, dass das Paket keine Abhängigkeiten haben darf. Da steht speziell(!) auch: Keine Abhängigkeiten zu Eclipse. Gut, es soll zwar ein Eclipse-Plugin sein, aber … naja, nichts wird so heiß gegessen, wie es gekocht wird :rolleyes: )