JFace-Viewer and Eclipse Databinding with > 10.000 Objects


Inspired by a bugzilla-discussion where virtual support for the nebula grid. I thought I blog a bit about Eclipse Databinding when it comes to big datasets.

The standard way used looks like this:

IObservable list = // ... a huge list of items

TableViewer viewer = new TableViewer(parent,SWT.BORDER|SWT.H_SCROLL|SWT.V_SCROLL);
ObservableListContentProvider cp = new ObservableListContentProvider();
viewer.setContentProvider(cp);

TableViewerColumn column = new TableViewerColumn(viewer,SWT.NONE);
column.getColumn().setText("Firstname");
column.getColumn().setWidth(200);
column.setLabelProvider(new ObservableMapCellLabelProvider(
    BeanProperties.value("firstname").observeDetail(cp.getKnownElements())
  )
);

TableViewerColumn column = new TableViewerColumn(viewer,SWT.NONE);
column.getColumn().setText("Lastname");
column.getColumn().setWidth(200);
column.setLabelProvider(new ObservableMapCellLabelProvider(
    BeanProperties.value("lastname").observeDetail(cp.getKnownElements())
  )
);

viewer.setInput(list);

Executing this code with the following item count shows the following figures:

Object count Time to fill (in ms)
10,000 1,178
30,000 2,793
100,000 17,689
200,000 62,689

So as we might have guess the performance degrades with the number of items with put in the viewer. In SWT you normally get the advise to use SWT.VIRTUAL but I don’t think that will address all your problems. One of them is the freaking high number of Listeners attached to your domain-objects. For 200.000 items above Eclipse Databinding has created 400.000 listeners (200.000 rows x 2 columns) not think about what would have happened if you’d have 10 columns.

So in reality there are 2 problems we have to address:

  • get the time of the initial creation down
  • only attach listeners only to those elements visible in the viewer

As outlined for SWT-Table you could address the 1st problem using SWT.VIRTUAL but for the 2nd there’s no solution. For Nebula-Grid the situation is different you don’t have SWT.VIRTUAL but instead of that some much more powerful. You can attach a listener and get informed about all visible cells so we can address both problems.

Let’s first look at the figures:

Object count Time to fill (in ms)
Original Range tracking
10,000 1,178 111
30,000 2,793 246
100,000 17,689 518
200,000 62,689 1,298

So the numbers have improved dramatically but the other really nice thing is that because by viewer shows in my test e.g. only 24 rows I have a total of 48 listeners attached to my domain objects.

The code is not really complex:

public class ViewOptimized extends ViewPart {
  public static final String ID = "at.bestsolution.grid.databinding.view";

  private GridTableViewer viewer;
	
  static final String ITEM_VISIBLE = "ITEM_VISIBLE";
	
  private static int LISTENERCOUNT = 0;
	
  public static class Person {
    private PropertyChangeSupport support = new PropertyChangeSupport(this);
		
    private String firstName;
    private String lastName;
		
    public void addPropertyChangeListener(PropertyChangeListener listener) {
      support.addPropertyChangeListener(listener);
      LISTENERCOUNT++;
    }
		
    public void removePropertyChangeListener(PropertyChangeListener listener) {
      support.removePropertyChangeListener(listener);
      LISTENERCOUNT--;
    }

    public Person(String firstName, String lastname) {
      this.firstName = firstName;
      this.lastName = lastname;
    }
		
    public String getFirstName() {
      return firstName;
    }

    public void setFirstName(String firstName) {
      support.firePropertyChange("firstName", this.firstName, 
        this.firstName = firstName
      );
    }
		
    public String getLastName() {
      return lastName;
    }

    public void setLastName(String lastName) {
      support.firePropertyChange("lastName", this.lastName, 
        this.lastName = lastName
      );
    }
  }

  public void createPartControl(Composite parent) {
    parent.setLayout(new GridLayout());
    this.viewer = new GridTableViewer(parent);
    this.viewer.getGrid().setHeaderVisible(true);
    this.viewer.getGrid().setLayoutData(new GridData(GridData.FILL_BOTH));
		
    viewer.setContentProvider(new ObservableListContentProvider());
    ObservableSet obserableSet = new WritableSet();
		
    GridVisibleRangeSupport rangeSupport = 
      GridVisibleRangeSupport.createFor(viewer.getGrid());
    rangeSupport.addRangeChangeListener(
      new VisibleRangeListenerImpl(viewer,obserableSet)
    );
		
    {
      GridViewerColumn col = new GridViewerColumn(viewer, SWT.NONE);
      col.getColumn().setText("Firstname");
      col.getColumn().setWidth(200);
      col.setLabelProvider(new LazyColumnLabelProvider(
        BeanProperties.value("firstName").observeDetail(obserableSet))
      );
    }
		
    {
      GridViewerColumn col = new GridViewerColumn(viewer, SWT.NONE);
      col.getColumn().setText("Lastname");
      col.getColumn().setWidth(200);
      col.setLabelProvider(new LazyColumnLabelProvider(
        BeanProperties.value("lastName").observeDetail(obserableSet))
      );
    }

    final ObservableList list = new WritableList();
    for( int i = 0; i < ViewOriginal.TOTAL_ITEMS; i++ ) {
      list.add(new Person("Tom " + i,"Schindl"));
    }
		
    Composite composite = new Composite(parent, SWT.NONE);
    composite.setLayout(new RowLayout());
		
    {
      Button b = new Button(composite, SWT.PUSH);
      b.setText("Populate it");
      b.addSelectionListener(new SelectionAdapter() {
        @Override
        public void widgetSelected(SelectionEvent e) {
          long t1 = System.currentTimeMillis();
          viewer.setInput(list);
          long t2 = System.currentTimeMillis();
          System.err.println("Rendering table took: " + (t2 - t1));
        }
      });
    }
		
    {
      Button b = new Button(composite, SWT.PUSH);
      b.setText("Modify First Object");
      b.addSelectionListener(new SelectionAdapter() {
        @Override
        public void widgetSelected(SelectionEvent e) {
          ((Person)list.get(0)).setFirstName("Hello World");
        }
      });
    }

    {
      Button b = new Button(composite, SWT.PUSH);
      b.setText("Stats");
      b.addSelectionListener(new SelectionAdapter() {
        @Override
        public void widgetSelected(SelectionEvent e) {
          System.err.println("Listeners: " + LISTENERCOUNT);
        }
      });
    }
  }

  public void setFocus() {
    viewer.getControl().setFocus();
  }

  public static class LazyColumnLabelProvider extends 
    ObservableMapCellLabelProvider {

    public LazyColumnLabelProvider(IObservableMap attributeMap) {
      super(attributeMap);
    }
		
    @Override
    public void update(ViewerCell cell) {
      Object visibleCheck = cell.getItem().getData(ITEM_VISIBLE);
      if( visibleCheck != null && ((Boolean)visibleCheck).booleanValue() ) {
        super.update(cell);
      }
    }
  }

  public static class VisibleRangeListenerImpl implements 
    VisibleRangeChangedListener {

    private ObservableSet obserableSet;
    private GridTableViewer viewer;
		
    public VisibleRangeListenerImpl(GridTableViewer viewer, 
      ObservableSet obserableSet
    ) {
      this.viewer = viewer;
      this.obserableSet = obserableSet;
    }
		
    @Override
    public void rangeChanged(RangeChangedEvent event) {
      for( GridItem i : event.removedRows ) {
        i.setData(ITEM_VISIBLE,Boolean.FALSE);
        obserableSet.remove(i.getData());
      }
			
      for( GridItem i : event.addedRows ) {
        i.setData(ITEM_VISIBLE,Boolean.TRUE);
        obserableSet.add(i.getData());
        viewer.update(i.getData(), null);
      }
    }	
  }
}

To round up the numbers here are those you get with plain SWT-Table with and without VIRTUAL:

Object count Time to fill (in ms)
Nebula Original Nebula Range tracking SWT Original SWT VIRTUAL
10,000 1,178 111 1,661 919
30,000 2,793 246 4,508 2,547
100,000 17,689 518 23,473 17,659
200,000 62,689 1,298 72,578 59,711
Advertisement
This entry was posted in Enhanced RCP. Bookmark the permalink.

6 Responses to JFace-Viewer and Eclipse Databinding with > 10.000 Objects

  1. Not Relevant says:

    What about this standard way?

    ViewerSupport.bind(viewer, list, new IValueProperty[] {BeanProperties.value(“firstname”), BeanProperties.value(“lastname”)});

    // void org.eclipse.jface.databinding.viewers.ViewerSupport.bind(StructuredViewer viewer, IObservableList input, IValueProperty[] labelProperties)

    • Tom Schindl says:

      Not sure what you mean with this comment the standard as you show it does under the covers exactly the same problematic API calls so you’ll have to same performance problems.

      • Not Relevant says:

        I was wondering why didn’t you use “ViewerSupport.bind” in the first piece of code when you mentioned the “standard way”. That’s all.

  2. Peer Törngren says:

    Hi, tried this out, and it does solve almost all problems. I still have some problems with my rendering, but that is simply due to slow label providers. However, I noticed a couple of things:

    1) When a tree is collapsed or the viewer input is set to null, items are disposed. You cannot do “i.setData(ITEM_VISIBLE,Boolean.FALSE);”, you get a widget disposed exception. I simply skip this step if item is disposed (if (!i.isDisposed()) [i.setData ….).

    2) If the label providers are slow, scrolling gets badly distorted. I use a delayed observable that delays the update until scrolling stops. The way I do it is to store the RangeChangedEvent in an observable property, and listen to this property using Observables.observeDelayedValue(….). This works quite nice, but it introduces an intermediate step + I need to manage (acccumulate and/or recalculate) additions and removals since I don’t act on each event.

    Suggestion: It would be very nice if the GridVisibleRangeSupport would let me specify a delay, and only fire events if the delay times out (I hope you understand what I mean?).

  3. Pingback: Eclipse JFace Table Databinding Scalability « Nigel's Eclipse Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.