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 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 |