PDA

View Full Version : grid performance with many rows -- FastGridView



ericwaldheim
28 Oct 2006, 5:12 PM
My app needs to quickly unload many rows from a grid and replace them with many other rows.
I've added a couple of optimizations to the previously posted lazy row rendering.
Basically I'm just trying to avoid the slow DOM calls.
For removing all rows from a grid (1000 rows x 3 columns), time went from 2.5 sec to 0.1 sec.
For inserting into an empty grid (1500 rows x 3 columns), time went from 3.0 sec to 0.5 sec.
(this is the time required to 'lazy render' the first two pages of rows.)

With all three optimizations time went from 18 sec to 1.3 sec (remove 1000 rows, add 1500 rows).

I've only tested this on Firefox.

Thanks,
Eric




YAHOO.ext.grid.FastGridView = function(){
YAHOO.ext.grid.FastGridView.superclass.constructor.call(this);
this.rendered = [];
};
YAHOO.extendX(YAHOO.ext.grid.FastGridView, YAHOO.ext.grid.GridView);

YAHOO.ext.grid.FastGridView.prototype.init = function(grid){
YAHOO.ext.grid.FastGridView.superclass.init.call(this, grid);
this.grid.maxRowsToMeasure = 1; // hack
this.grid.addListener('bodyscroll', this.lazyRender, this, true);
}

YAHOO.ext.grid.FastGridView.prototype.insertRows
= function(dataModel, firstRow, lastRow)
{
this.updateBodyHeight();
this.adjustForScroll(true);
var renderers = this.getColumnRenderers();
var dindexes = this.getDataIndexes();
var colCount = this.grid.colModel.getColumnCount();
var beforeRow = null;
var bt = this.getBodyTable();
if(firstRow < bt.childNodes.length){
beforeRow = bt.childNodes[firstRow];
}
if (firstRow == 0 && lastRow == dataModel.getRowCount() - 1)
{
var s = "";
var stripeRows = this.grid.stripeRows;
var stripe = false;
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
var alt = stripe && stripeRows ? ' ygrid-row-alt' : '';
s += "<span class='ygrid-row"+ alt +"' style='top:"
+ (rowIndex * this.rowHeight) + "px' rowIndex:'"+rowIndex
+"'></span>";
this.rendered[rowIndex] = false;
stripe = !stripe;
}
bt.innerHTML = bt.innerHTML + s;
}
else
{
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
var row = document.createElement('span');
row.className = 'ygrid-row';
row.style.top = (rowIndex * this.rowHeight) + 'px';
this.rendered[rowIndex] = false;
if(beforeRow){
bt.insertBefore(row, beforeRow);
}else{
bt.appendChild(row);
}
}
this.updateRowIndexes(firstRow);
}
this.adjustForScroll();
this.lazyRender();
};

YAHOO.ext.grid.FastGridView.prototype.lazyRender = function()
{
var y = this.wrap.scrollTop;
var rowHeight = this.getRowHeight();
var rowIndex = (y == 0 ? 0 : Math.floor(y / rowHeight));
var grid = this.grid;
var dataModel = grid.getDataModel();
var rowCount = dataModel.getRowCount();
if (rowIndex >= rowCount ){ return; }
var numDisplayedRows = Math.ceil(this.wrap.clientHeight / rowHeight);
var lastRowIndex = Math.min(rowCount - 1, rowIndex + numDisplayedRows * 2);

var colCount = grid.colModel.getColumnCount();
var renderers = this.getColumnRenderers();
var dindexes = this.getDataIndexes();
while (rowIndex <= lastRowIndex)
{
if (!this.rendered[rowIndex])
{
var row = grid.getRow(rowIndex);
this.renderRow(dataModel, row, rowIndex, colCount, renderers, dindexes);
this.rendered[rowIndex] = true;
}
++rowIndex;
}
}


YAHOO.ext.grid.FastGridView.prototype.deleteRows
= function(dataModel, firstRow, lastRow)
{
this.updateBodyHeight();
// first make sure they are deselected
var bt = this.getBodyTable();

if (!this.grid.dataModel.getRowCount())
{
this.grid.selModel.clearSelections();
bt.innerHTML = "";
}
else
{
this.grid.selModel.deselectRange(firstRow, lastRow);
var rows = []; // get references because the rowIndex will change
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
rows.push(bt.childNodes[rowIndex]);
}
for(var i = 0; i < rows.length; i++){
bt.removeChild(rows[i]);
rows[i] = null;
}
rows = null;
this.updateRowIndexes(firstRow);
this.adjustForScroll();
}
};

jack.slocum
28 Oct 2006, 10:29 PM
That's awesome. Thanks for packaging it into a class too, that will make it much easier to test and include in the distribution. I am going to do some testing with it on IE and Opera and I'll let you know how it goess (I can't test Safari).

kalebwalton
29 Oct 2006, 12:09 PM
Oops!

Guess I should have read through the forums before posting my FastGridView just now (http://www.jackslocum.com/forum/viewtopic.php?t=430) :). Glad to see someone else had the same thoughts as I did! Nice job -

ericwaldheim
30 Oct 2006, 9:05 AM
Found a bug.
I was trying to set the span's rowIndex while building the html string.


for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
var alt = stripe && stripeRows ? ' ygrid-row-alt' : '';
s += "<span class='ygrid-row"+ alt +"' style='top:"
+ (rowIndex * this.rowHeight) + "px' rowIndex:'"+rowIndex
+"'></span>";


This evidently does not work.
So I pulled the "rowIndex:..." code and added a loop after setting innerHTML:


bt.innerHTML = s;
var rows = bt.childNodes;
for (var rowIndex = 0; rowIndex < rows.length; rowIndex++)
{
rows[rowIndex].rowIndex = rowIndex;
}


Is there a way to do this without the additional loop?

Thanks.
Eric

tryanDLS
30 Oct 2006, 9:43 AM
Given the amount of string concatenation that's going on here, you might try something else to get more speed.
I don't have any numbers on relative speed of tons of string concat vs arrays, but I think it will be faster.

Replace
s = '';
for ... {
s = s + 'abc' + 'def';
s = s + 'xyz'...
}
...innerHTML = s;


with
s = [];
for .. {
s[s.length] = 'abc';
s[s.length] = 'def';
s[s.length] = 'xyz';
}
...innerHTML = s.join('');

ericwaldheim
30 Oct 2006, 9:49 AM
Thanks, I'll change it.

http://www.comet.co.il/en/articles/performance/article.html

ericwaldheim
30 Oct 2006, 10:01 AM
(redundant. removed.)

ericwaldheim
30 Oct 2006, 11:01 AM
updateRows needs to be modified as well since it assumes all rows are already rendered.

the latest:




YAHOO.ext.grid.FastGridView = function(){
YAHOO.ext.grid.FastGridView.superclass.constructor.call(this);
this.rendered = [];
};
YAHOO.extendX(YAHOO.ext.grid.FastGridView, YAHOO.ext.grid.GridView);

YAHOO.ext.grid.FastGridView.prototype.init = function(grid){
YAHOO.ext.grid.FastGridView.superclass.init.call(this, grid);
this.grid.maxRowsToMeasure = 1; // hack
this.grid.addListener('bodyscroll', this.lazyRender, this, true);
}

YAHOO.ext.grid.FastGridView.prototype.insertRows
= function(dataModel, firstRow, lastRow)
{
this.updateBodyHeight();
this.adjustForScroll(true);
var renderers = this.getColumnRenderers();
var dindexes = this.getDataIndexes();
var colCount = this.grid.colModel.getColumnCount();
var beforeRow = null;
var bt = this.getBodyTable();
if(firstRow < bt.childNodes.length){
beforeRow = bt.childNodes[firstRow];
}
if (firstRow == 0 && lastRow == dataModel.getRowCount() - 1)
{
var s = [];
var stripeRows = this.grid.stripeRows;
var stripe = false;
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
s[s.length] = "<span class='ygrid-row";
s[s.length] = stripe && stripeRows ? ' ygrid-row-alt' : '';
s[s.length] = "' style='top:";
s[s.length] = rowIndex * this.rowHeight;
s[s.length] = "px'></span>";
this.rendered[rowIndex] = false;
stripe = !stripe;
}
bt.innerHTML = s.join("");
var rows = bt.childNodes;
for (var rowIndex = 0; rowIndex < rows.length; rowIndex++)
{
rows[rowIndex].rowIndex = rowIndex;
}
}
else
{
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
var row = document.createElement('span');
row.className = 'ygrid-row';
row.style.top = (rowIndex * this.rowHeight) + 'px';
this.rendered[rowIndex] = false;
if(beforeRow){
bt.insertBefore(row, beforeRow);
}else{
bt.appendChild(row);
}
}
this.updateRowIndexes(firstRow);
}
this.adjustForScroll();
this.lazyRender();
};

YAHOO.ext.grid.FastGridView.prototype.lazyRender = function()
{
var indexes = this.getRowsToRender();
var rowIndex = indexes[0];
var lastRowIndex = indexes[1];
var grid = this.grid;
var dataModel = grid.getDataModel();
var colCount = grid.colModel.getColumnCount();
var renderers = this.getColumnRenderers();
var dindexes = this.getDataIndexes();
while (rowIndex <= lastRowIndex)
{
if (!this.rendered[rowIndex])
{
var row = grid.getRow(rowIndex);
this.renderRow(dataModel, row, rowIndex, colCount, renderers, dindexes);
this.rendered[rowIndex] = true;
}
++rowIndex;
}
}

YAHOO.ext.grid.FastGridView.prototype.getRowsToRender = function()
{
var y = this.wrap.scrollTop;
var rowHeight = this.getRowHeight();
var rowIndex = (y == 0 ? 0 : Math.floor(y / rowHeight));
var grid = this.grid;
var dataModel = grid.getDataModel();
var rowCount = dataModel.getRowCount();
if (rowIndex >= rowCount) { return [0, -1]; }
var numDisplayedRows = Math.ceil(this.wrap.clientHeight / rowHeight);
var lastRowIndex = Math.min(rowCount - 1, rowIndex + numDisplayedRows * 2);
return [rowIndex, lastRowIndex];
}


YAHOO.ext.grid.FastGridView.prototype.deleteRows
= function(dataModel, firstRow, lastRow)
{
this.updateBodyHeight();
// first make sure they are deselected
var bt = this.getBodyTable();

if (!this.grid.dataModel.getRowCount())
{
this.grid.selModel.clearSelections();
bt.innerHTML = "";
}
else
{
this.grid.selModel.deselectRange(firstRow, lastRow);
var rows = []; // get references because the rowIndex will change
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
rows.push(bt.childNodes[rowIndex]);
}
for(var i = 0; i < rows.length; i++){
bt.removeChild(rows[i]);
rows[i] = null;
}
rows = null;
this.updateRowIndexes(firstRow);
this.adjustForScroll();
}
};

YAHOO.ext.grid.FastGridView.prototype.updateRows
= function(dataModel, firstRow, lastRow)
{
var bt = this.getBodyTable();
var dindexes = this.getDataIndexes();
var renderers = this.getColumnRenderers();
var grid = this.grid;
var colCount = grid.colModel.getColumnCount();
var indexes = this.getRowsToRender();
var firstRenderedRow = indexes[0];
var lastRenderedRow = indexes[1];
for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
var row = bt.rows[rowIndex];
if (rowIndex >= firstRenderedRow && rowIndex <= lastRenderedRow)
{
if (!this.rendered[rowIndex])
{
var row = grid.getRow(rowIndex);
this.renderRow(dataModel, row, rowIndex, colCount, renderers,
dindexes);
this.rendered[rowIndex] = true;
}
else
{
var cells = row.childNodes;
for(var colIndex = 0; colIndex < colCount; colIndex++){
var td = cells[colIndex];
var val = renderers[colIndex](dataModel.getValueAt(rowIndex, dindexes[colIndex]), rowIndex, colIndex);
if(typeof val == 'undefined' || val === '') val = '';
td.firstChild.innerHTML = val;
}
}
}
else
{
row.innerHTML = "";
this.rendered[rowIndex] = false;
}
}
};