Blog

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 countTime to fill (in ms)
------
10,0001,178
30,0002,793
100,00017,689
200,00062,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 countTime to fill (in ms)
OriginalRange tracking
10,0001,178111
30,0002,793246
100,00017,689518
200,00062,6891,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 countTime to fill (in ms)
Nebula OriginalNebula Range trackingSWT OriginalSWT VIRTUAL
10,0001,1781111,661919
30,0002,7932464,5082,547
100,00017,68951823,47317,659
200,00062,6891,29872,57859,711
published on
Previous
e(fx)clipse - LivePreview for fxgraph