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 |
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)
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.
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.
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?).
I see how this could help can you file me a enhancement request
Pingback: Eclipse JFace Table Databinding Scalability « Nigel's Eclipse Blog