Bug 1136896. Speed up fill() and dedent() by memoizing some of the work they currently end up doing on each call. r=jorendorff
authorBoris Zbarsky <bzbarsky@mit.edu>
Tue, 03 Mar 2015 07:12:00 -0500
changeset 247014 58f17236b1873e0fa556e112d4cb6385b5a5113f
parent 247013 0217de65664e855f2e5c90854cb0fd790ee0e930
child 247015 1ccaf7d21b90832217ea1ede61f3fc949642b321
push id884
push userdburns@mozilla.com
push dateTue, 03 Mar 2015 15:29:12 +0000
reviewersjorendorff
bugs1136896
milestone39.0a1
Bug 1136896. Speed up fill() and dedent() by memoizing some of the work they currently end up doing on each call. r=jorendorff
dom/bindings/Codegen.py
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -4,16 +4,17 @@
 
 # Common codegen classes.
 
 import os
 import re
 import string
 import math
 import textwrap
+import functools
 
 from WebIDL import BuiltinTypes, IDLBuiltinType, IDLNullValue, IDLSequenceType, IDLType, IDLAttribute, IDLInterfaceMember, IDLUndefinedValue, IDLEmptySequenceValue, IDLDictionary
 from Configuration import NoSuchDescriptorError, getTypesFromDescriptor, getTypesFromDictionary, getTypesFromCallback, getAllTypes, Descriptor
 
 AUTOGENERATED_WARNING_COMMENT = \
     "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n"
 ADDPROPERTY_HOOK_NAME = '_addProperty'
 FINALIZE_HOOK_NAME = '_finalize'
@@ -95,25 +96,91 @@ def indent(s, indentLevel=2):
     Weird secret feature: this doesn't indent lines that start with # (such as
     #include lines or #ifdef/#endif).
     """
     if s == "":
         return s
     return re.sub(lineStartDetector, indentLevel * " ", s)
 
 
+# dedent() and fill() are often called on the same string multiple
+# times.  We want to memoize their return values so we don't keep
+# recomputing them all the time.
+def memoize(fn):
+    """
+    Decorator to memoize a function of one argument.  The cache just
+    grows without bound.
+    """
+    cache = {}
+    @functools.wraps(fn)
+    def wrapper(arg):
+        retval = cache.get(arg)
+        if retval is None:
+            retval = cache[arg] = fn(arg)
+        return retval
+    return wrapper
+
+@memoize
 def dedent(s):
     """
     Remove all leading whitespace from s, and remove a blank line
     at the beginning.
     """
     if s.startswith('\n'):
         s = s[1:]
     return textwrap.dedent(s)
 
+
+# This works by transforming the fill()-template to an equivalent
+# string.Template.
+fill_multiline_substitution_re = re.compile(r"( *)\$\*{(\w+)}(\n)?")
+
+
+@memoize
+def compile_fill_template(template):
+    """
+    Helper function for fill().  Given the template string passed to fill(),
+    do the reusable part of template processing and return a pair (t,
+    argModList) that can be used every time fill() is called with that
+    template argument.
+
+    argsModList is list of tuples that represent modifications to be
+    made to args.  Each modification has, in order: i) the arg name,
+    ii) the modified name, iii) the indent depth.
+    """
+    t = dedent(template)
+    assert t.endswith("\n") or "\n" not in t
+    argModList = []
+
+    def replace(match):
+        """
+        Replaces a line like '  $*{xyz}\n' with '${xyz_n}',
+        where n is the indent depth, and add a corresponding entry to
+        argModList.
+
+        Note that this needs to close over argModList, so it has to be
+        defined inside compile_fill_template().
+        """
+        indentation, name, nl = match.groups()
+        depth = len(indentation)
+
+        # Check that $*{xyz} appears by itself on a line.
+        prev = match.string[:match.start()]
+        if (prev and not prev.endswith("\n")) or nl is None:
+            raise ValueError("Invalid fill() template: $*{%s} must appear by itself on a line" % name)
+
+        # Now replace this whole line of template with the indented equivalent.
+        modified_name = name + "_" + str(depth)
+        argModList.append((name, modified_name, depth))
+        return "${" + modified_name + "}"
+
+    t = re.sub(fill_multiline_substitution_re, replace, t)
+    return (string.Template(t), argModList)
+
+
 def fill(template, **args):
     """
     Convenience function for filling in a multiline template.
 
     `fill(template, name1=v1, name2=v2)` is a lot like
     `string.Template(template).substitute({"name1": v1, "name2": v2})`.
 
     However, it's shorter, and has a few nice features:
@@ -133,50 +200,23 @@ def fill(template, **args):
 
         A `$*` substitution must appear by itself on a line, with optional
         preceding indentation (spaces only). The whole line is replaced by the
         corresponding keyword argument, indented appropriately.  If the
         argument is an empty string, no output is generated, not even a blank
         line.
     """
 
-    # This works by transforming the fill()-template to an equivalent
-    # string.Template.
-    multiline_substitution_re = re.compile(r"( *)\$\*{(\w+)}(\n)?")
-
-    def replace(match):
-        """
-        Replaces a line like '  $*{xyz}\n' with '${xyz_n}',
-        where n is the indent depth, and add a corresponding entry to args.
-        """
-        indentation, name, nl = match.groups()
-        depth = len(indentation)
-
-        # Check that $*{xyz} appears by itself on a line.
-        prev = match.string[:match.start()]
-        if (prev and not prev.endswith("\n")) or nl is None:
-            raise ValueError("Invalid fill() template: $*{%s} must appear by itself on a line" % name)
-
-        # Multiline text without a newline at the end is probably a mistake.
+    t, argModList = compile_fill_template(template)
+    # Now apply argModList to args
+    for (name, modified_name, depth) in argModList:
         if not (args[name] == "" or args[name].endswith("\n")):
             raise ValueError("Argument %s with value %r is missing a newline" % (name, args[name]))
-
-        # Now replace this whole line of template with the indented equivalent.
-        modified_name = name + "_" + str(depth)
-        indented_value = indent(args[name], depth)
-        if modified_name in args:
-            assert args[modified_name] == indented_value
-        else:
-            args[modified_name] = indented_value
-        return "${" + modified_name + "}"
-
-    t = dedent(template)
-    assert t.endswith("\n") or "\n" not in t
-    t = re.sub(multiline_substitution_re, replace, t)
-    t = string.Template(t)
+        args[modified_name] = indent(args[name], depth)
+
     return t.substitute(args)
 
 
 class CGThing():
     """
     Abstract base class for things that spit out code.
     """
     def __init__(self):