Adventures with JavaScript Objects

I have to start with an admission — I’m not a trained JavaScript programmer.  This became evident to me as I reviewed the latest version of the exclusion group sample form.  I was not happy with the performance, so I thought I would stick in a timer and see if I could measure and improve performance.  Adding the timer was no problem.  I changed the code on the subform remove button.  Note the time calculations at the beginning and end:

form1.#subform[0].NPSA3.NPTable.Row1.Cell14::click – (JavaScript, client)

var vTime1 = (new Date()).getTime(); // gets the time in milliseconds

// Removing a subform in the middle of a slie can
// change the SOM expressions of subforms later in the list
// This means that we have to clear the error list
scValidate.clearErrList();

if (_Row1.count > _Row1.min)
    _Row1.removeInstance(Row1.index);
// rebuild the error list
xfa.form.execCalculate();
xfa.form.execValidate();

var vTime2 = (new Date()).getTime();
console.println(vTime2 – vTime1);

 

For my test I added 20 subform rows.  With a total of 21 rows there are 65 exclusion groups on the form.  Deleting a row causes all 65 groups to be re-evaluated.  (actually most of them get evaluated twice, but that is a bug behavior that I cannot control).  With the measurement in place I could see that deleting one row was taking 2753 milliseconds on my laptop.  As I deleted more, the deletions sped up — since there were fewer groups to evaluate each time.  By the time I was down to two rows, the delete took 454 milliseconds.  All much too slow.

It did not take long for me to realize that my problem was the JavaScript objects I had defined to implement exclusion group behavior.  Here is where I expose my JavaScript naiveté.  I wanted to define an abstract group definition so that the implementation could be either based on a subform or on an array group.  When I set up the base class, I followed a C++ pattern.  I defined a base class with methods that the derived classes would override:

function exclusionGroup()
{
}
exclusionGroup.prototype.length = function() {return 0;}
exclusionGroup.prototype.item   = function(nItem) {return null;}
exclusionGroup.prototype.getMax = function() {return null;}
exclusionGroup.prototype.getMin = function() {return null;}

One of the derived classes:

// Implementation of exclusionGroup for a subform container
function subformGroup(vSubform)
{
    this.mSubform = vSubform;
}
subformGroup.prototype         = new exclusionGroup();

subformGroup.prototype.length  = function()        
    {return this.mSubform.nodes.length;};

subformGroup.prototype.getMax  = function()        
    {return this.mSubform.vMaxSelected.value;}

subformGroup.prototype.getMin  = function()        
    {return this.mSubform.vMinSelected.value;}

subformGroup.prototype.selectedContainer = function()
{
    if (this.mSubform.vOldSelectedContainer.value == "")
        return null;
    return this.mSubform.resolveNode(this.mSubform.vOldSelectedContainer.value);
}

What I failed to take into account is that JavaScript is completely interpreted.  e.g. When the getMax() method on the derived class gets called, it matters none that the method exists in a base class.  The interpreter simply checks the class instance to see if it has the getMax() method.  The base class was just extra processing overhead with no benefit.  The base class might have had some benefit if we had shared implementations of some methods — but we didn’t. So I removed the base class.  I created two classes: subformGroup and arrayGroup that both define the same member variables and functions.  There is no type checking.  The onus is on the programmer to make sure the property names, methods and parameters all match. 

The other revelation is that I did not need to extend the objects using prototype.  Using prototype impacts the class definition — i.e. all instances of a class.  You can extend a single instance of a class without bothering with prototype.  That seemed to improve performance as well.

In the revised form, my object definitions looked like:

// Implementation of exclusionGroup for a subform container
function subformGroup(vSubform)
{
    this.mSubform          = vSubform;
    this.len               = vSubform.nodes.length;
    this.maximum           = vSubform.vMaxSelected.value;
    this.minimum           = vSubform.vMinSelected.value;
    this.selectedContainer = function()
    {
        if (this.mSubform.vOldSelectedContainer.value == "")
            return null;
        return this.mSubform.resolveNode
                    (this.mSubform.vOldSelectedContainer.value);
    } 
    …
}

// Implementation of exclusionGroup for an array group
function arrayGroup(vHost)
{
    this.mHost             = vHost;
    this.len               = vHost.length.value; 
    this.maximum           = vHost.max.value;
    this.minimum           = vHost.min.value;
    this.selectedContainer = function()
    {
        if (this.mHost.vOldSelectedContainer.value == "")
            return null;

        return this.mHost.parent.resolveNode
                   (this.mHost.vOldSelectedContainer.value);
    }
    …
}

Simplifying the object definitions had a big impact on performance.  Deleting the 21st subform went from 2753 milliseconds to 1236 milliseconds.  Well over a 2x improvement.  Still not fast enough for my liking, but good enough for one round of performance tuning.  Now maybe I’ll go out and buy a JavaScript book…