Bug 702642 - DOMTemplate is relatively slow when evaluating JS ${}; r=dcamp
authorJoe Walker <jwalker@mozilla.com>
Thu, 08 Dec 2011 12:37:20 +0000
changeset 82259 f1304b596193016b01374c4d5772281323ea82a0
parent 82258 71054aef1a3a3bd2a01440a47a4571831c8caee9
child 82260 c84bdc1137e1c91dca9332f9176ee547a1ffd24a
push id21589
push usertim.taubert@gmx.de
push dateFri, 09 Dec 2011 04:57:11 +0000
treeherdermozilla-central@9e7239c0f557 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs702642
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 702642 - DOMTemplate is relatively slow when evaluating JS ${}; r=dcamp
browser/devtools/shared/Templater.jsm
--- a/browser/devtools/shared/Templater.jsm
+++ b/browser/devtools/shared/Templater.jsm
@@ -40,23 +40,64 @@
 var EXPORTED_SYMBOLS = [ "Templater" ];
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 const Node = Components.interfaces.nsIDOMNode;
 
 // WARNING: do not 'use_strict' without reading the notes in _envEval();
 
 /**
- * A templater that allows one to quickly template DOM nodes.
+ * Begin a new templating process.
+ * @param node A DOM element or string referring to an element's id
+ * @param data Data to use in filling out the template
+ * @param options Options to customize the template processing. One of:
+ * - allowEval: boolean (default false) Basic template interpolations are
+ * either property paths (e.g. ${a.b.c.d}), however if allowEval=true then we
+ * allow arbitrary JavaScript
  */
-function Templater() {
+function template(node, data, options) {
+  var template = new Templater(options || {});
+  template.processNode(node, data);
+  return template;
+}
+
+/**
+ * Construct a Templater object. Use template() in preference to this ctor.
+ * @deprecated Use template(node, data, options);
+ */
+function Templater(options) {
+  if (options == null) {
+    options = { allowEval: true };
+  }
+  this.options = options;
   this.stack = [];
 }
 
 /**
+ * Cached regex used to find ${...} sections in some text.
+ * Performance note: This regex uses ( and ) to capture the 'script' for
+ * further processing. Not all of the uses of this regex use this feature so
+ * if use of the capturing group is a performance drain then we should split
+ * this regex in two.
+ */
+Templater.prototype._templateRegion = /\$\{([^}]*)\}/g;
+
+/**
+ * Cached regex used to split a string using the unicode chars F001 and F002.
+ * See Templater._processTextNode() for details.
+ */
+Templater.prototype._splitSpecial = /\uF001|\uF002/;
+
+/**
+ * Cached regex used to detect if a script is capable of being interpreted
+ * using Template._property() or if we need to use Template._envEval()
+ */
+Templater.prototype._isPropertyScript = /^[a-zA-Z0-9.]*$/;
+
+/**
  * Recursive function to walk the tree processing the attributes as it goes.
  * @param node the node to process. If you pass a string in instead of a DOM
  * element, it is assumed to be an id for use with document.getElementById()
  * @param data the data to use for node processing.
  */
 Templater.prototype.processNode = function(node, data) {
   if (typeof node === 'string') {
     node = document.getElementById(node);
@@ -106,17 +147,17 @@ Templater.prototype.processNode = functi
             node.removeAttribute(name);
             var capture = node.hasAttribute('capture' + name.substring(2));
             node.addEventListener(name.substring(2), func, capture);
             if (capture) {
               node.removeAttribute('capture' + name.substring(2));
             }
           } else {
             // Replace references in all other attributes
-            var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
+            var newValue = value.replace(this._templateRegion, function(path) {
               return this._envEval(path.slice(2, -1), data, value);
             }.bind(this));
             // Remove '_' prefix of attribute names so the DOM won't try
             // to use them before we've processed the template
             if (name.charAt(0) === '_') {
               node.removeAttribute(name);
               node.setAttribute(name.substring(1), newValue);
             } else if (value !== newValue) {
@@ -290,18 +331,18 @@ Templater.prototype._processTextNode = f
   // attribute processing in processNode()) because we need to support
   // functions that return DOM nodes, so we can't have the conversion to a
   // string.
   // Instead we process the string as an array of parts. In order to split
   // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
   // We can then split using \uF001 or \uF002 to get an array of strings
   // where scripts are prefixed with $.
   // \uF001 and \uF002 are just unicode chars reserved for private use.
-  value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
-  var parts = value.split(/\uF001|\uF002/);
+  value = value.replace(this._templateRegion, '\uF001$$$1\uF002');
+  var parts = value.split(this._splitSpecial);
   if (parts.length > 1) {
     parts.forEach(function(part) {
       if (part === null || part === undefined || part === '') {
         return;
       }
       if (part.charAt(0) === '$') {
         part = this._envEval(part.slice(1), data, node.data);
       }
@@ -358,17 +399,17 @@ Templater.prototype._handleAsync = funct
 };
 
 /**
  * Warn of string does not begin '${' and end '}'
  * @param str the string to check.
  * @return The string stripped of ${ and }, or untouched if it does not match
  */
 Templater.prototype._stripBraces = function(str) {
-  if (!str.match(/\$\{.*\}/g)) {
+  if (!str.match(this._templateRegion)) {
     this._handleError('Expected ' + str + ' to match ${...}');
     return str;
   }
   return str.slice(2, -1);
 };
 
 /**
  * Combined getter and setter that works with a path through some data set.
@@ -422,27 +463,36 @@ Templater.prototype._property = function
  * the values in the env object. This is likely to be slow, but workable.
  * @param script The string to be evaluated.
  * @param data The environment in which to eval the script.
  * @param frame Optional debugging string in case of failure.
  * @return The return value of the script, or the error message if the script
  * execution failed.
  */
 Templater.prototype._envEval = function(script, data, frame) {
-  with (data) {
-    try {
-      this.stack.push(frame);
-      return eval(script);
-    } catch (ex) {
-      this._handleError('Template error evaluating \'' + script + '\'' +
-          ' environment=' + Object.keys(data).join(', '), ex);
-      return script;
-    } finally {
-      this.stack.pop();
+  try {
+    this.stack.push(frame);
+    if (this._isPropertyScript.test(script)) {
+      return this._property(script, data);
+    } else {
+      if (!this.options.allowEval) {
+        this._handleError('allowEval is not set, however \'' + script + '\'' +
+            ' can not be resolved using a simple property path.');
+        return '${' + script + '}';
+      }
+      with (data) {
+        return eval(script);
+      }
     }
+  } catch (ex) {
+    this._handleError('Template error evaluating \'' + script + '\'' +
+        ' environment=' + Object.keys(data).join(', '), ex);
+    return '${' + script + '}';
+  } finally {
+    this.stack.pop();
   }
 };
 
 /**
  * A generic way of reporting errors, for easy overloading in different
  * environments.
  * @param message the error message to report.
  * @param ex optional associated exception.