[Laszlo-checkins] r10921 - in openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc: core services

ptw@openlaszlo.org ptw at openlaszlo.org
Tue Sep 9 07:10:43 PDT 2008


Author: ptw
Date: 2008-09-09 07:10:40 -0700 (Tue, 09 Sep 2008)
New Revision: 10921

Modified:
   openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/core/LzNode.lzs
   openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/services/LzCSSStyle.js
Log:
Change 20080908-ptw-7 by ptw at dueling-banjos.local on 2008-09-08 14:18:03 EDT
    in /Users/ptw/OpenLaszlo/pagan-deities/WEB-INF/lps/lfc
    for http://svn.openlaszlo.org/openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc

Summary: Optimize CSS application

Bugs Fixed:
LPP-6938 - Improve CSS performance (partial)

Technical Reviewer: max (Message-ID: <48C5C4A9.6050507 at openlaszlo.org>)
QA Reviewer: pablo (pending)

Details:
    Reorganize LzCSSStyle heavily to try to optimize it.  Removed most
    of the previous inlining so profiling could actually see where the
    time was going.  Really the only inlining needed is the
    short-circuit that determines that a compound rule can't be
    anchored on a node, which means you can skip the full compound
    rule check altogether.

    Removed lots of old cruft, put lots of things into local vars, did
    more cacheing up front, rewrote inner loops to hoist constant
    computations, simplified applicability computation to use known
    invariants about parsed rules.  Simplified specificity calculation
    to do easiest cases first and return early.

    Hand-inlined getPropertyValueFor into __LZapplyStyleMap.  Now that
    the CSS properties are all cached up front, this loop can run much
    faster to install the properties as needed without repeated
    function calls.

Tests:
    style/metasuite still passes, Max's comparison showed significant
    speed up.



Modified: openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/core/LzNode.lzs
===================================================================
--- openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/core/LzNode.lzs	2008-09-09 08:30:07 UTC (rev 10920)
+++ openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/core/LzNode.lzs	2008-09-09 14:10:40 UTC (rev 10921)
@@ -499,15 +499,28 @@
   * @access private
   */
 function __LZapplyStyleMap( stylemap, initialArgs ){
-    var styleConstraints = {};    
+    var styleConstraints = {};
+    // Inlined version of LzCSSStyle.getPropertyValueFor
+    // Get the property cache for this node
+    var pc = this['__LZPropertyCache'] || LzCSSStyle.getPropertyCache(this);
     for ( var k in stylemap ){
-        //we are going to bypass the CSS API and call the underlying
-        //implementation because we're concerned about speed
         if (initialArgs[k] != null) {
             // Don't get CSS value if it would be overridden by a instance/class attribute later
             continue;
         }
-        var v = LzCSSStyle.getPropertyValueFor( this , stylemap[ k ] );
+        var pname = stylemap[k];
+        var v;
+        if (pname in pc) {
+          v = pc[pname];
+        } else {
+          if ($debug) {
+            // Fix for LPP-3024: if we're in debug mode, warn when CSS lookup
+            // results in a null or undefined value. [bshine 08.03.2007]
+            Debug.warn("No CSS value found for node %#w for property name %s", this, pname);
+          }
+          // Cache negative values lazily
+          v = pc[pname] = null;
+        }
 
         // This is a hack because people want to give color styles as
         // Ox... which is not valid CSS, so they pass it as a string.

Modified: openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/services/LzCSSStyle.js
===================================================================
--- openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/services/LzCSSStyle.js	2008-09-09 08:30:07 UTC (rev 10920)
+++ openlaszlo/branches/pagan-deities/WEB-INF/lps/lfc/services/LzCSSStyle.js	2008-09-09 14:10:40 UTC (rev 10921)
@@ -22,54 +22,49 @@
 //No rule has a specificity of zero
 LzCSSStyleRule.prototype.specificity = 0;
 
-// this is inlined in __compareSpecificity() - keep in sync
+// Called by addRule, once the rule is parsed, so it can be cached
 LzCSSStyleRule.prototype.getSpecificity = function () {
-    
-    // Only calculate this once. Store it after we've calculated it once.     
+
+    // Only calculate this once. Store it after we've calculated it once.
     // No rule has a specificity of 0
     if ( !this.specificity ) {
+      var s = 0;
+      var p = this.parsed;
+
         //need to treat compound selectors differently
 
-        if ( this.parsed.type == LzCSSStyle._sel_compound ){
-            for ( var i = 0; i < this.parsed.length; i++ ){
-                this.specificity += LzCSSStyle.getSelectorSpecificity( this.parsed[ i ] );
+        if ( p.type == LzCSSStyle._sel_compound ){
+          for ( var i = 0, l = p.length; i < l; i++ ){
+                s += LzCSSStyle.getSelectorSpecificity( p[ i ] );
             }
         } else {
-            this.specificity = LzCSSStyle.getSelectorSpecificity( this.parsed );
+            s = LzCSSStyle.getSelectorSpecificity( p );
         }
-
+        this.specificity = s;
         //Debug.write( 'specificity for' , this , this.specificity );
     }
 
-    return this.specificity; 
+    return this.specificity;
 }
 
-/** For a simple selector, figure out the specificity based on the type of the
-  * element. We determine the type of the simple selector with some quick and
-  * easy checks for sentinel characters. 
-  * @access private
-  */ 
-LzCSSStyleRule._getSimpleSelectorSpecificity = function ( selarr, k ) {
-    var sel = selarr[k];
-    //Debug.write( 'selarr' , selarr , "sel" , sel );
-
-    // If this simple selector includes an element type, that counts for
-    // specificity of +1. 
-    if (k == "simpleselector") {
-        return 1; 
+/** @access private */
+LzCSSStyleRule.prototype._dbg_name = function () {
+  function pname (rp) {
+    var rpcn = rp.classname;
+    var rpi = rp['id'];
+    var rpa = rp['attrname'];
+    if (! (rpcn||rpi|rpa)) { return '*'; }
+    return (rpcn?rpcn:'')+(rpi?('#'+rpi):'')+(rpa?('['+rpa+'='+rp.attrvalue+']'):'')
+  }
+  var rp = this.parsed;
+  if (rp['length']) {
+    var n = '';
+    for (var i = 0; i < rp.length; i++) {
+      n += pname(rp[i]) + ' ';
     }
-    
-    if (sel.indexOf("#") >= 0) {
-        // it's an id selector
-        return 100; 
-    }
-    
-    if (sel.indexOf("[") >= 0) {
-        // it's an attr=value selector
-        return 10; 
-    }
-    
-    return 1 ;
+    return n.substring(0, n.length-1);
+  }
+  return pname(rp) + ' (' + this.specificity + ')';
 }
 
 var LzCSSStyle = {};
@@ -78,180 +73,193 @@
     var csssd = new LzCSSStyleDeclaration( );
     csssd.setNode( node );
     return csssd;
+};
+
+// Hand-inlined in LzNode/__LZapplyStyleMap
+// Must be kept in sync
+LzCSSStyle.getPropertyValueFor = function (node, pname) {
+  var pc = node['__LZPropertyCache'];
+  if (! pc) { pc = this.getPropertyCache(node); }
+  if (pname in pc) {
+    return pc[pname];
+  }
+  if ($debug) {
+    // Fix for LPP-3024: if we're in debug mode, warn when CSS lookup
+    // results in a null or undefined value. [bshine 08.03.2007]
+    Debug.warn("No CSS value found for node %#w for property name %s", node, pname);
+  }
+  // Cache negative values lazily
+  return pc[pname] = null;
 }
 
-//LzCSSStyle.time1 = 0;
-LzCSSStyle.getPropertyValueFor = function ( initialnode , pname ){
-    //Debug.warn("node: %w, pname: %w, rules: %w\n", node, pname, this._rules);
 
-    //var t = getTimer();
-    if (!initialnode || !pname) return;
+LzCSSStyle.getPropertyCache = function ( node ) {
+    if (node === canvas) return {};
+    var pc = node['__LZPropertyCache'];
+    if ( pc ) { return pc; }
+    // The cache is initialized from the parent, then our rules are
+    // added.
+    var ip = node.immediateparent;
+    if (ip === canvas) {
+      var ipc = {};
+    } else {
+      var ipc = ip['__LZPropertyCache'] || this.getPropertyCache(ip);
+    }
+    var rules = node['__LZRuleCache'];
+    if ( !rules ) { rules = this.getRulesCache(node); }
 
-    var node = initialnode;
-    while (node != canvas) {
-        if (! node.__LZPropertyCache) {
-            node.__LZPropertyCache = {};
-        } else {
-            var val = node.__LZPropertyCache[pname];
-            if (val != null) return val;
-        }
+    // If we have no rules, we just share the parent cache
+    if (rules.length == 0) {
+      return node.__LZPropertyCache = ipc;
+    }
 
-        var rules = node.__LZRuleCache;
-        if ( !rules ) {
-            rules = new Array();
-            var r;
-            for ( var i = this._rules.length -1; i >=0; i-- ){
-                r = this._rules[ i ];
-                var rp = r.parsed;
-                //inlined: if (rp.type == this._sel_compound && this._compoundSelectorApplies( rp, node )) {
-                if (rp.type == this._sel_compound) {
-                    var curnode = node; 
-                    var sindex = rp.length - 1;
-                    var firstone = true;
-                    var result = false;
+    // If we have rules, copy the parent cache and add the properties
+    // our rules define
+    var pc = {};
+    for (var k in ipc) { pc[k] = ipc[k]; }
+    // Process the rules from least to most specific, so the
+    // most-specific win
+    for (var i = rules.length - 1; i >= 0; i--)  {
+      var props = rules[i].properties;
+      for (var pname in props) {
+        pc[pname] = props[pname];
+      }
+    }
+    return node.__LZPropertyCache = pc;
+}
 
-                    while (curnode != canvas){
-                        //recursively loop through selectors, ensuring each applies to the current node or a parent 
-                        var nrp = rp[sindex]
-                        t = nrp.type;
+// Will we ever use this cache more than once?
+LzCSSStyle.getRulesCache = function (node) {
+  var rules = node['__LZRuleCache'];
+  if (rules) { return rules; }
+  rules = new Array();
+  var tryRules = new Array();
+  // Repeat for rules, tag rules, attr rules, id rules; accumulating
+  // the rules that might apply (which are filtered more carefully in
+  // the second pass).
 
-                        if ( t == this._sel_star ||
-                            (t == this._sel_id && nrp.id == curnode.id) ||
-                            (t == this._sel_tag && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-                            (t == this._sel_attribute && curnode[ nrp.attrname ] == nrp.attrvalue) ||
-                            (t == this._sel_tagAndAttr && curnode[ nrp.attrname ] == nrp.attrvalue && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-                            (t == this._sel_compound && this._compoundSelectorApplies( nrp, curnode, true ))){
-                            if ( sindex-- == 0 ){
-                                result = true;
-                                break;
-                            }
-                        } else if ( firstone ){
-                            //if the last selector doesn't apply, then bail -- we'll
-                            //come back for this when we recurse over the parents in
-                            //getPropertyValueFor
-                            result = false;
-                            break;
-                        }
+  /**
+   * NOTE: [2008-09-08 ptw] These rules are gathered in approximate
+   * specificity order, to minimize the cost of the sort below.  Take
+   * great care if you muck with this code that you don't screw that
+   * up!
+   */
+  // Ensure the rule cohorts are in order
+  if (this._rulenum != this._lastSort) {
+    this._sortRules();
+  }
 
-                        curnode = curnode.parent;
-                        firstone = false;
-                    }
-                    if (result) rules.push(r); 
-                } else if ( rp.type == this._sel_star ||
-                    (rp.type == this._sel_id && rp.id == node.id) ||
-                    (rp.type == this._sel_tag && (((rp.classname in lz) && (node instanceof lz[ rp.classname ])) || ( (rp.classname in global) && (node instanceof global[ rp.classname ])))) ||
-                    (rp.type == this._sel_attribute && node[ rp.attrname ] == rp.attrvalue) ||
-                    (rp.type == this._sel_tagAndAttr && node[ rp.attrname ] == rp.attrvalue && (((rp.classname in lz) && (node instanceof lz[ rp.classname ])) || ( (rp.classname in global) && (node instanceof global[ rp.classname ]))))) {
-                    rules.push(r); 
-                }
-            }
+  // Gather the id rules that could apply
+  var id = node['id'];
+  // Does this node have an id with rules?
+  if (id) {
+    var ir = this._idRules;
+    if (id in ir) {
+      tryRules.push.apply(tryRules, ir[id]);
+    }
+  }
 
-            //now look at any preprocessed rules
-            // NOTE: 
-            // it would be nice to just use (node.name) as the condition, but 
-            // swf6 does not obey obey the ECMA string->boolean coercion specification.
-            // As a workaround, we compare node.name to null, thereby ensuring we get a boolean
-            // in swf6 or ECMA-compatible runtimes
-            var pprules = (node.name != null) ? this._nameRules[ node.name ] : null;
-            if ( pprules ){
-                //same code as above, but inline to avoid function call overhead
-                for ( var i = pprules.length -1; i >=0; i-- ){
-                    r = pprules[ i ];
-                    var rp = r.parsed;
-                    //inlined: if (rp.type == this._sel_compound && this._compoundSelectorApplies( rp, node )) {
-                    if (rp.type == this._sel_compound) {
-                        var curnode = node; 
-                        var sindex = rp.length - 1;
-                        var firstone = true;
-                        var result = false;
+  // Gather the attribute rules that could apply
+  var ar = this._attrRules;
+  for (var k in ar) {
+    // Does this node have that attribute?
+    if (k in node) {
+      var rs = ar[k];
+      // Is this an attribute with rules?
+      if (rs instanceof Array) {
+        // Could filter on property value now... but you have to worry
+        // about compound rules
+        tryRules.push.apply(tryRules, rs);
+      }
+    }
+  }
 
-                        while (curnode != canvas){
-                            //recursively loop through selectors, ensuring each applies to the current node or a parent 
-                            var nrp = rp[sindex]
-                            t = nrp.type;
+  // Gather the tag rules that could apply
+  var cr = this._tagRules;
+  for (var cn in cr) {
+    var c = lz[cn];
+    // Can we filter these any further here?
+    if (c && (node instanceof c)) {
+      tryRules.push.apply(tryRules, cr[cn]);
+    }
+  }
 
-                            if ( t == this._sel_star ||
-                                (t == this._sel_id && nrp.id == curnode.id) ||
-                                (t == this._sel_tag && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-                                (t == this._sel_attribute && curnode[ nrp.attrname ] == nrp.attrvalue) ||
-                                (t == this._sel_tagAndAttr && curnode[ nrp.attrname ] == nrp.attrvalue && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-                                (t == this._sel_compound && this._compoundSelectorApplies( nrp, curnode, true ))){
-                                if ( sindex-- == 0 ){
-                                    result = true;
-                                    break;
-                                }
-                            } else if ( firstone ){
-                                //if the last selector doesn't apply, then bail -- we'll
-                                //come back for this when we recurse over the parents in
-                                //getPropertyValueFor
-                                result = false;
-                                break;
-                            }
+  // Are there any of these any more?
+  var rr = this._rules;
+  for (var i = rr.length - 1; i >= 0; i--) {
+    tryRules.push(rr[i]);
+  }
 
-                            curnode = curnode.parent;
-                            firstone = false;
-                        }
-                        if (result) rules.push(r); 
-                    } else if ( rp.type == this._sel_star ||
-                        (rp.type == this._sel_id && rp.id == node.id) ||
-                        (rp.type == this._sel_tag && (((rp.classname in lz) && (node instanceof lz[ rp.classname ])) || ( (rp.classname in global) && (node instanceof global[ rp.classname ])))) ||
-                        (rp.type == this._sel_attribute && node[ rp.attrname ] == rp.attrvalue) ||
-                        (rp.type == this._sel_tagAndAttr && node[ rp.attrname ] == rp.attrvalue && (((rp.classname in lz) && (node instanceof lz[ rp.classname ])) || ( (rp.classname in global) && (node instanceof global[ rp.classname ]))))) {
-                        rules.push(r); 
-                    }
-                }
-            }
-            
-            //sort match rules by their specificities
-            //first tell the sort function who this is being sorted for; the sort
-            //depends not only on the rules, but also on the specificity of the
-            //class selector, if one is present
-
-            rules.sort(this.__compareSpecificity); 
-        
-            node.__LZRuleCache = rules;
+  // Now winnow the rules by applicability, at the same time, see if
+  // any sorting is needed
+  var sortNeeded = false;
+  var lastSpecificity = Infinity;
+  for ( var i = 0, l = tryRules.length; i < l; i++ ) {
+    var r = tryRules[ i ];
+    if (! sortNeeded) {
+      var rs = r.specificity;
+      if ((! rs) || (rs >= lastSpecificity)) {
+        sortNeeded = true;
+      } else {
+        lastSpecificity = rs;
+      }
+    }
+    var rp = r.parsed;
+    var rpt = rp.type;
+    var compound = (rpt == this._sel_compound);
+    // Quick test for compound _not_ applying:  If the last selector
+    // is not applicable, no need to even try.  It will either apply
+    // to our parent (already cached) or a child, but not us.
+    if (compound) {
+      // Look at the last selector
+      rp = rp[rp.length - 1];
+      rpt = rp.type;
+    }
+    var rpcn = rp.classname;
+    // Classes get defined lazily, way after rules are parsed, so
+    // we have to look this up each time. But NOTE, in the
+    // optimization below, we only check the class if there is a
+    // class NAME.
+    var rpc = rpcn ? lz[rpcn] : null;
+    var rpi = rp['id'];
+    var rpa = rp['attrname'];
+    // Optimized test for applicability: we can ignore type because
+    // if a rule has a class, id, or attrname, they have to apply,
+    // and a 'star' rule has none of those
+    if (((! rpcn) || (rpc && node instanceof rpc)) &&
+        ((! rpi) || (node['id'] == rpi)) &&
+        ((! rpa) || (node[rpa] == rp.attrvalue))) {
+      if (! compound) {
+        rules.push(r);
+      } else {
+        // Last selector applies, so verify that the whole rule
+        // applies.  (Need to go back to original rule to do this.)
+        if (this._compoundSelectorApplies( r.parsed, node )) {
+          rules.push(r);
         }
-
-        //Debug.write("About to print rule array.") 
-        //LzCSSStyle._printRuleArray( rules );
-    
-        var l = rules.length;
-        var i = 0; 
-        while ( i < l ) {
-            var props = rules[i++].properties;
-            if (pname in props) { 
-                val = props[pname];
-                node.__LZPropertyCache[pname] = val;
-                break;
-            }
-        }
-        if (val != null) return val;
-
-        node = node.immediateparent;
-        ////this.time1 += getTimer() - t;
+      }
     }
+  }
 
-    if ($debug) {
-        // Fix for LPP-3024: if we're in debug mode, warn when CSS lookup
-        // results in a null or undefined value. [bshine 08.03.2007]
-        Debug.warn(
-            "No CSS value found for node %#w for property name %s",
-            initialnode,
-            pname);             
-    }
+  if (sortNeeded) {
+//     if ($debug) {
+//       var s = "";
+//       for (var i = 0, l = rules.length; i < l; i++) {
+//         s += rules[i].specificity + ' ';
+//       }
+//       Debug.debug("Sorting %w (%s)", rules, s);
+//     }
+    rules.sort(this.__compareSpecificity);
+  } else if ($debug) {
+    Debug.debug("Saved a sort! %w", rules);
+  }
+
+  node.__LZRuleCache = rules;
+  return rules;
 }
 
-// this is inlined in __compareSpecificity() - keep in sync
+
 LzCSSStyle.getSelectorSpecificity = function ( parsedsel ){
-    // Go through all the selectors in the selector, keeping a running
-    // count of various kinds of selectors:
-    /*
-    count 1 if the selector is a `style` attribute rather than a selector, 
-        0 otherwise (= a) 
-    count the number of ID attributes in the selector (= b) 
-    count the number of other attributes and pseudo-classes in the selector (= c) 
-    count the number of element names and pseudo-elements in the selector (= d) 
-    */ 
     switch ( parsedsel.type ){
         case (this._sel_tag ):
         case (this._sel_star ):
@@ -264,228 +272,192 @@
             return 10;
 
         case (this._sel_tagAndAttr ):
-            return 11; 
+            return 11;
     }
-}
+};
 
 /** @access private */
 LzCSSStyle.__compareSpecificity = function (rA, rB) {
-    // if rA specificity  is less than rB specifity, return -1
-    // if they're equal specificity, return 0
-    // if rB specifitity is less than rA specificity, return 1
-    if (! rA.specificity) {
-        //inline: rA.getSpecificity();
-        // Only calculate this once. Store it after we've calculated it once.     
-        // No rule has a specificity of 0
-        if ( !rA.specificity ) {
-            //need to treat compound selectors differently
+  // Specificity is stored when the rule is parsed
+  var specificityA = rA.specificity;
+  var specificityB = rB.specificity;
 
-            if ( rA.parsed.type == LzCSSStyle._sel_compound ){
-                for ( var i = 0; i < rA.parsed.length; i++ ){
-                    //inline: rA.specificity += LzCSSStyle.getSelectorSpecificity( rA.parsed[ i ] );
-                    switch ( rA.parsed[i].type ){
-                        case (LzCSSStyle._sel_tag ):
-                        case (LzCSSStyle._sel_star ):
-                            rA.specificity += 1;
-                            break;
+  // Simplest case: they have different specificity
+  if ( specificityA != specificityB ) {
+    return (specificityA < specificityB) ? 1 : -1;
+  }
 
-                        case (LzCSSStyle._sel_id ):
-                            rA.specificity += 100;
-                            break;
+  // Ties that involve tag selectors are broken by comparing the
+  // specificity of the classes of the tags
+  var rap = rA.parsed;
+  var rbp = rB.parsed;
+  var lexorder = (rA._lexorder < rB._lexorder ) ? 1 : -1;
 
-                        case (LzCSSStyle._sel_attribute ):
-                            rA.specificity += 10;
-                            break;
+  // Simple rules have no length
+  if ((! rap['length']) && (! rbp['length'])) {
+    // Simple tag rules have a class name property
+    var racn = rap['classname'];
+    var rbcn = rbp['classname'];
 
-                        case (LzCSSStyle._sel_tagAndAttr ):
-                            rA.specificity += 11; 
-                            break;
-                    }
-                }
-                } else {
-                    //inline: rA.specificity = LzCSSStyle.getSelectorSpecificity( rA.parsed );
-                    switch ( rA.parsed.type ){
-                        case (LzCSSStyle._sel_tag ):
-                        case (LzCSSStyle._sel_star ):
-                            rA.specificity = 1;
-                            break;
-
-                        case (LzCSSStyle._sel_id ):
-                            rA.specificity = 100;
-                            break;
-
-                        case (LzCSSStyle._sel_attribute ):
-                            rA.specificity = 10;
-                            break;
-
-                        case (LzCSSStyle._sel_tagAndAttr ):
-                            rA.specificity = 11; 
-                            break;
-                    }
-                }
-
-                //Debug.write( 'specificity for' , this , this.specificity );
-        }
+    // Simpler case: Not tag rules, or the classes are the same
+    if ((! racn) || (! rbcn) || (racn == rbcn)) {
+      return lexorder;
     }
-    if (! rB.specificity) {
-        //inline: rB.getSpecificity();
-        // Only calculate this once. Store it after we've calculated it once.     
-        // No rule has a specificity of 0
-        if ( !rB.specificity ) {
-            //need to treat compound selectors differently
 
-            if ( rB.parsed.type == LzCSSStyle._sel_compound ){
-                for ( var i = 0; i < rB.parsed.length; i++ ){
-                    //inline: rB.specificity += LzCSSStyle.getSelectorSpecificity( rB.parsed[ i ] );
-                    switch ( rB.parsed[i].type ){
-                        case (LzCSSStyle._sel_tag ):
-                        case (LzCSSStyle._sel_star ):
-                            rB.specificity += 1;
-                            break;
-
-                        case (LzCSSStyle._sel_id ):
-                            rB.specificity += 100;
-                            break;
-
-                        case (LzCSSStyle._sel_attribute ):
-                            rB.specificity += 10;
-                            break;
-
-                        case (LzCSSStyle._sel_tagAndAttr ):
-                            rB.specificity += 11; 
-                            break;
-                    }
-                }
-            } else {
-                //inline: rB.specificity = LzCSSStyle.getSelectorSpecificity( rB.parsed );
-                switch ( rB.parsed.type ){
-                    case (LzCSSStyle._sel_tag ):
-                    case (LzCSSStyle._sel_star ):
-                        rB.specificity = 1;
-                        break;
-
-                    case (LzCSSStyle._sel_id ):
-                        rB.specificity = 100;
-                        break;
-
-                    case (LzCSSStyle._sel_attribute ):
-                        rB.specificity = 10;
-                        break;
-
-                    case (LzCSSStyle._sel_tagAndAttr ):
-                        rB.specificity = 11; 
-                        break;
-                }
-            }
-
-            //Debug.write( 'specificity for' , this , this.specificity );
-        }
+    // Simple case: Neither compound
+    //  Push comes to shove -- the classes had better exist now
+    var rac = lz[racn];
+    var rbc = lz[rbcn];
+    // Subclass test
+    // TODO: [2008-09-09 ptw] Will this work in JS2?
+    if (rac && rbc) {
+      if (rac['prototype'] instanceof rbc) { return -1; }
+      if (rbc['prototype'] instanceof rac) { return 1; }
     }
+    // The classes are not comparable, default
+    return lexorder;
+  }
 
-    var specificityA = rA.specificity;
-    var specificityB = rB.specificity;
-    //Debug.write( rA, specificityA );
-    //Debug.write( rB, specificityB );
-
-    if ( specificityA == specificityB ){
-        // Laszlo has a special rules around applicability of rules. In the
-        // case where two selectors have the same specifity, AND both select
-        // classes, the one that applies to the closer class in the inheritance
-        // hierarchy wins
-
-        // if *that* matches, then the descendant rule with closer selectors
-        // wins
-
-        if ( rA.parsed.type == LzCSSStyle._sel_compound && 
-             rB.parsed.type == LzCSSStyle._sel_compound ){
-            //iterate through the compound selector arrays 
-            //assume that the selector arrays are the same length, due to
-            //specificity. if they don't, or if all the comparisons match, then
-            //we drop down to lexical order
-            for ( var i = 0; i < rA.parsed.length; i++ ){
-                //if we get here, it means that two rules have the same
-                //specificity but different numbers of descendants
-                if ( !rA.parsed[ i ] || !rB.parsed[ i ] ) break;
-
-                //if one or neither selector has a classname, this test doesn't
-                //apply OR the classnames are the same
-                if ( !rA.parsed[ i ].classname || !rB.parsed[ i ].classname ||
-                      rA.parsed[ i ].classname ==  rB.parsed[ i ].classname ){
-                    continue;
-                }
-
-                var rac = lz[ rA.parsed[ i ].classname ];
-                var rbc = lz[ rB.parsed[ i ].classname ];
-                
-                
-                return (((rac && rbc) && ('prototype' in rac) && (rac.prototype instanceof rbc)) ? -1 : 1);
-                
-
-            }
-        } 
-
-        if ( ( rA.parsed.classname && rB.parsed.classname ) &&
-             ( rA.parsed.classname != rB.parsed.classname ) ){
-                var rac = lz[ rA.parsed.classname ];
-                var rbc = lz[ rB.parsed.classname ];
-                
-                return (((rac && rbc)
-                        && ('prototype' in rac) && 
-                        (rac.prototype instanceof rbc)) 
-                    ? -1 : 1);
-
-        } else return (rA._lexorder < rB._lexorder ) ? 1 : -1;
+  // Hard case: Compound rule must be examined step-by-step
+  for ( var i = 0; i < rap.length; i++ ) {
+    var rapi = rap[i];
+    var rbpi = rbp[i];
+    // if we get here, it means that two rules have the same
+    // specificity but different numbers of descendants?  That
+    // should not be able to happen
+    if ( !rapi || !rbpi ) {
+      if ($debug) {
+        Debug.debug("%s: %w <=> %w", arguments.callee, rA, rB);
+      }
+      // Punt to lexical order
+      break;
     }
-
-    return (specificityA < specificityB) ? 1 : -1;
+    //  Classes get defined lazily, way after rules are
+    //  parsed, so we have to defer evaluating them
+    var racn = rapi['classname'];
+    var rbcn = rbpi['classname'];
+    if ( ( racn && rbcn ) && ( racn != rbcn ) ) {
+      //  Push comes to shove -- the classes had better exist now
+      var rac = lz[racn];
+      var rbc = lz[rbcn];
+      // Subclass test
+      // TODO: [2008-09-09 ptw] Will this work in JS2?
+      if (rac && rbc) {
+        if (rac['prototype'] instanceof rbc) { return -1; }
+        if (rbc['prototype'] instanceof rac) { return 1; }
+      }
+      // The classes are not comparable, keep going
+    }
+  }
+  // Last ditch
+  return lexorder;
 }
 
+
 /** @access private */
 LzCSSStyle._printRuleArray = function (arr) {
     for (var i = 0; i < arr.length; i++) {
         Debug.write(i, arr[i]);
     }
-    
-}
 
-/** @access private 
+};
+
+/** @access private
  *  This is inlined above in getPropertyValueFor() - make sure they stan in sync
  */
-LzCSSStyle._compoundSelectorApplies = function (rp , node){
-    var curnode = node; 
-    var sindex = rp.length - 1;
-    var firstone = true;
-    var result = false;
-
-    while (curnode != canvas){
-        //recursively loop through selectors, ensuring each applies to the current node or a parent 
-        var nrp = rp[sindex]
-        var t = nrp.type;
-
-        if ( t == this._sel_star ||
-            (t == this._sel_id && nrp.id == curnode.id) ||
-            (t == this._sel_tag && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-            (t == this._sel_attribute && curnode[ nrp.attrname ] == nrp.attrvalue) ||
-            (t == this._sel_tagAndAttr && curnode[ nrp.attrname ] == nrp.attrvalue && (((nrp.classname in lz) && (curnode instanceof lz[ nrp.classname ])) || ( (nrp.classname in global) && (curnode instanceof global[ nrp.classname ])))) ||
-            (t == this._sel_compound && this._compoundSelectorApplies( nrp, curnode, true ))){
-            if ( sindex-- == 0 ){
-                result = true;
-                break;
-            }
-        } else if ( firstone ){
-            //if the last selector doesn't apply, then bail -- we'll
-            //come back for this when we recurse over the parents in
-            //getPropertyValueFor
-            result = false;
-            break;
+LzCSSStyle._compoundSelectorApplies = function (parsedsel , startnode) {
+  // loop through selectors, ensuring each applies to the current node or a parent
+  for (var node = startnode, i = parsedsel.length - 1; i >= 0 && node !== canvas; i--, node = node.parent) {
+    // Components of a parsed compound rule are parsed
+    var rp = parsedsel[i];
+    var rpcn = rp.classname;
+    // Classes get defined lazily, way after rules are parsed, so
+    // we have to look this up each time. But NOTE, in the
+    // optimization below, we only check the class if there is a
+    // class NAME.
+    var rpc = rpcn ? lz[rpcn] : null;
+    var rpi = rp['id'];
+    var rpa = rp['attrname'];
+    // look up the nodes to see if this selector applies to the
+    // current node or a parent
+    while (node !== canvas) {
+      // Optimized test for applicability: we can ignore type because
+      // if a rule has a class, id, or attrname, they have to apply,
+      // and a 'star' rule has none of those
+      //
+      // NB: The old code tested for a compound selector in a
+      // compound selector, but that can't really happen, can it?
+      if (((! rpcn) || (rpc && node instanceof rpc)) &&
+          ((! rpi) || (node.id == rpi)) &&
+          ((! rpa) || (node[rpa] == rp.attrvalue))) {
+        // Match
+        if ( i == 0 ){
+          // Total match
+          return true;
+        } else {
+          // Match next
+          break;
         }
-
-        curnode = curnode.parent;
-        firstone = false;
+      } else {
+        if (node === startnode) {
+          // If the selector can't be anchored here, then exit.  If it
+          // applied in a parent, it will have already been examined
+          // and cached
+          return false;
+        }
+      }
+      // Go up a node and try to match again
+      // TODO: [2008-09-06 ptw] parent, not immediateparent?
+      node = node.parent;
     }
-    return result;
+  }
+  // Got to the canvas or end of the rule without a match
+  return false;
 }
+// LzCSSStyle._compoundSelectorApplies = function (tr , startnode){
+//     var node = startnode; 
+//     var sindex = tr.length - 1;
+//     var firstone = true;
+//     var result = false;
 
+//     while (node != canvas){
+//       // loop through selectors, ensuring each applies to the current node or a parent 
+//       var rp = tr[sindex];
+//       var rpcn = rp.classname;
+//       // Classes get defined lazily, way after rules are parsed, so
+//       // we have to look this up each time...
+//       var rpc = rpcn ? lz[rpcn] : null;
+//       var rpi = rp['id'];
+//       var rpa = rp['attrname'];
+//       // Optimized test for applicability: we can ignore type because
+//       // if a rule has a class, id, or attrname, they have to apply,
+//       // and a 'star' rule has none of those
+//       //
+//       // NB: The old code tested for a compound selector in a
+//       // compound selector, but that can't really happen, can it?
+//       if ( rp.type == this._sel_star ||
+//            (rp.type == this._sel_id && rp.id == node.id) ||
+//            (rp.type == this._sel_tag && (((rp.classname in lz) && (node instanceof lz[ rp.classname ])))) ||
+//            (rp.type == this._sel_attribute && node[ rp.attrname ] == rp.attrvalue) ||
+//            (rp.type == this._sel_tagAndAttr && node[ rp.attrname ] == rp.attrvalue && (((rp.classname in lz) && (node instanceof lz[ rp.classname ]))))) {
+//         if ( sindex-- == 0 ){
+//           result = true;
+//           break;
+//         }
+//       } else if ( firstone ){
+//         // if the last selector doesn't apply, then bail -- we'll come
+//         // back for this when we recurse over the parents in
+//         // getPropertyValueFor
+//         result = false;
+//         break;
+//       }
+
+//       node = node.parent;
+//       firstone = false;
+//     }
+//     return result;
+// }
+
 /** @access private */
 LzCSSStyle._sel_unknown = 0;
 /** @access private */
@@ -504,14 +476,41 @@
 /** @access private */
 LzCSSStyle._rules = new Array();
 
-/** optimization for selectors which use [name="value"]
+/** optimization for selectors which use [<attr>=<value>]
   * @access private */
-LzCSSStyle._nameRules = {};
+LzCSSStyle._attrRules = {};
 
+/** optimization for selectors which use #id
+  * @access private */
+LzCSSStyle._idRules = {};
+
+/** optimization for selectors which use tagname
+  * @access private */
+LzCSSStyle._tagRules = {};
+
 /** @access private */
-LzCSSStyle._rulenum = 0
+LzCSSStyle._rulenum = 0;
 
 /** @access private */
+LzCSSStyle._lastSort = -1;
+
+LzCSSStyle._sortRules = function () {
+  if (this._rulenum != this._lastSort) {
+    this._rules.sort(this.__compareSpecificity);
+    for (var k in this._attrRules) {
+      this._attrRules[k].sort(this.__compareSpecificity);
+    }
+    for (var k in this._idRules) {
+      this._idRules[k].sort(this.__compareSpecificity);
+    }
+    for (var k in this._tagRules) {
+      this._tagRules[k].sort(this.__compareSpecificity);
+    }
+    this._lastSort = this._rulenum;
+  }
+}
+
+/** @access private */
 LzCSSStyle._addRule = function ( r ){
     //do some preprocessing to speed up lookups
     r._lexorder = this._rulenum++;
@@ -526,22 +525,36 @@
             r.parsed.push( this._parseSelector( sel[ i ] ) );
         }
         lastsel = r.parsed[ r.parsed.length -1 ];
-    } else { 
+    } else {
         r.parsed = this._parseSelector( sel );
         lastsel = r.parsed;
     }
 
-    //special treatment for rules that use name=
-    //we could do this pretty easily for ID if ID rules were common,
-    //or for other attibute names
+    // Cache this now
+    r.getSpecificity();
+
+    // Sort rules into different cohorts to reduce the number of rules
+    // we have to test for applicability in getRulesCache
     if ( ( lastsel.type == this._sel_attribute ||
-           lastsel.type == this._sel_tagAndAttr )
-           && lastsel.attrname == "name" ){
-        var aval = lastsel.attrvalue;
-        if ( !this._nameRules[ aval ] ) this._nameRules[ aval ] = [];
-          this._nameRules[ aval ].push( r );
+           lastsel.type == this._sel_tagAndAttr ) ) {
+      // Since an attr rule is higher specificity than a tag rule, we
+      // put attr+tag rules here and filter on the attr first.
+      var attr = lastsel.attrname;
+      if ( !this._attrRules[ attr ] ) { this._attrRules[ attr ] = []; }
+      this._attrRules[ attr ].push( r );
+    } else if (lastsel.type == this._sel_id) {
+      var id = lastsel.id;
+      if ( !this._idRules[ id ] ) { this._idRules[ id ] = []; }
+      this._idRules[ id ].push( r );
+    } else if (lastsel.type == this._sel_tag) {
+      var tag = lastsel.classname;
+      if ( !this._tagRules[ tag ] ) { this._tagRules[ tag ] = []; }
+      this._tagRules[ tag ].push( r );
     } else {
-        this._rules.push( r );
+      if ($debug) {
+        Debug.error("Unknown cohort for rule: %w", r);
+      }
+      this._rules.push( r );
     }
 }
 
@@ -551,8 +564,8 @@
         case "object":
             if (sel.simpleselector) {
                 sel.type = this._sel_tagAndAttr;
-                sel.classname = this._normalizeClassname( sel.simpleselector );
-            } else     
+                sel.classname = sel.simpleselector;
+            } else
                 sel.type = this._sel_attribute;
             return sel;
             break;
@@ -568,14 +581,14 @@
     if ( sel == "*" ) {
         parsed.type = this._sel_star;
     } else {
-        var index = sel.indexOf("#"); 
+        var index = sel.indexOf("#");
         if (index >= 0) {
             // Assumption: there should only be one # in a selector
-            parsed.id =  sel.substring(index + 1);        
+            parsed.id =  sel.substring(index + 1);
             parsed.type =  this._sel_id;
         } else {
             parsed.type =  this._sel_tag;
-            parsed.classname = this._normalizeClassname( sel );
+            parsed.classname = sel;
         }
 
     }
@@ -583,31 +596,6 @@
     return parsed;
 }
 
-
-/** The tag name of some nodes doesn't match the associated class name, ie, 
-  * nodes declared with tag &lt;view&gt; are instances of LzView, not instances of view.
-  * This function normalizes tag names into class names. The complete tag to classname
-  * mapping is listed in LaszloInitiator.as. For speed and size, I am only actually 
-  * mapping the class names which it seems sane to style. I am excluding data-related
-  * classes, because they will have undefined behavior when used together with CSS.
-  * @access private
- */ 
-LzCSSStyle._normalizeClassname = function ( cn ){
-    switch (cn) {
-        case "view":        return "LzView";
-        case "animator":    return "LzAnimator";
-        case "animatorgroup": return "LzAnimatorGroup";
-        case "canvas":      return  "LzCanvas";
-        case "drawview":    return "LzDrawView";
-        case "inputtext":   return "LzInputText";
-        case "layout":      return "LzLayout";
-        case "node":        return "LzNode";
-        case "state":       return "LzState";
-        case "text":        return "LzText";
-        default: return cn;
-    }
-}
-
 /** These objects implement
   * Interface CSSStyleDeclaration (introduced in DOM Level 2) from
   * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration



More information about the Laszlo-checkins mailing list