[PATCH 3 of 6] revset: implement precomputed revset functions

Patrick Mezard patrick at mezard.eu
Tue May 8 15:58:08 CDT 2012


# HG changeset patch
# User Patrick Mezard <patrick at mezard.eu>
# Date 1336509824 -7200
# Node ID dbeeb20515c9b9225f74fc8491d516a7b6b2f48a
# Parent  53db775e252c0f4f7add32b62e08096814d3ab7e
revset: implement precomputed revset functions

Calling revset matchers more than once works correctly but could be
inefficient when the revset functions have to generate expensive data
structures before starting to filter input revisions, the structures
being recreated at each call. A precomputed() decorator is introduced,
marking functions as returning filtering closures rather than filtering
directly. Unfortunately, the expression tree is made of tuples which
cannot be extended to preserve state. The preparetree() function
traverses the expression tree and replaces ('func', fname, ...) tuples
with funcobj objects, extending tuple, and able to handle the
@precomputed functions. All this is applied to _ancestors() as an
example.

The goal is to turn all functions used by graphlog.getlogrevs() into
precomputed ones.

On mozilla repository:

                         first line    total
  * hg log --follow -r 90000:80000
  2.2                        0.127s   1.667s
  before                     0.366s   6.139s
  after                      0.375s   1.783s

diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -47,6 +47,40 @@
                 yield i
                 break
 
+def precomputed(func):
+    """Decorator marking a revset function as precomputed
+
+    Instead of returning a list of revisions, the function returns a
+    closure doing so, usually to preserve expensive state between
+    calls. This is useful when revset matchers are being called
+    repeatedly instead of once.
+    """
+    func.precomputed = True
+    return func
+
+class funcobj(tuple):
+    """Replace ('func', 'name', ...) tuples in the expression tree.
+
+    funcobj preserves state between calls to an existing revset tree,
+    like revset function precomputed data.
+    """
+    iscallable = True
+
+    def __init__(self, *args, **kwargs):
+        super(funcobj, self).__init__(*args, **kwargs)
+        self.fn = None
+
+    def __call__(self, repo, subset, a, b):
+        if a[0] == 'symbol' and a[1] in symbols:
+            fn = symbols[a[1]]
+            if self.fn is None:
+                if getattr(fn, 'precomputed', False):
+                    self.fn = fn(repo, subset, b)
+                else:
+                    self.fn = fn
+            return self.fn(repo, subset, b)
+        raise error.ParseError(_("not a function: %s") % a[1])
+
 elements = {
     "(": (20, ("group", 1, ")"), ("func", 1, ")")),
     "~": (18, None, ("ancestor", 18)),
@@ -153,6 +187,8 @@
 def getset(repo, subset, x):
     if not x:
         raise error.ParseError(_("missing argument"))
+    if getattr(x, 'iscallable', False):
+        return x(repo, subset, *x[1:])
     return methods[x[0]](repo, subset, *x[1:])
 
 # operator methods
@@ -237,19 +273,27 @@
 
     return [r for r in an if r in subset]
 
+ at precomputed
 def _ancestors(repo, subset, x, followfirst=False):
     args = getset(repo, range(len(repo)), x)
-    if not args:
-        return []
-    s = set(_revancestors(repo, args, followfirst)) | set(args)
-    return [r for r in subset if r in s]
+    s = set()
+    if args:
+        s = set(_revancestors(repo, args, followfirst)) | set(args)
 
+    def fn(repo, subset, x):
+        if not s:
+            return []
+        return [r for r in subset if r in s]
+    return fn
+
+ at precomputed
 def ancestors(repo, subset, x):
     """``ancestors(set)``
     Changesets that are ancestors of a changeset in set.
     """
     return _ancestors(repo, subset, x)
 
+ at precomputed
 def _firstancestors(repo, subset, x):
     # ``_firstancestors(set)``
     # Like ``ancestors(set)`` but follows only the first parents.
@@ -1369,6 +1413,14 @@
 
 parse = parser.parser(tokenize, elements).parse
 
+def preparetree(tree):
+    if isinstance(tree, tuple):
+        kind = tuple
+        if tree and tree[0] == 'func':
+            kind = funcobj
+        return kind(preparetree(e) for e in tree)
+    return tree
+
 def match(ui, spec):
     if not spec:
         raise error.ParseError(_("empty query"))
@@ -1378,6 +1430,7 @@
     if ui:
         tree = findaliases(ui, tree)
     weight, tree = optimize(tree, True)
+    tree = preparetree(tree)
     def mfunc(repo, subset):
         return getset(repo, subset, tree)
     return mfunc


More information about the Mercurial-devel mailing list