<p dir="ltr"><br>
On Aug 28, 2012 12:31 AM, <<a href="mailto:pierre-yves.david@logilab.fr">pierre-yves.david@logilab.fr</a>> wrote:<br>
><br>
> # HG changeset patch<br>
> # User Pierre-Yves David <<a href="mailto:pierre-yves.david@logilab.fr">pierre-yves.david@logilab.fr</a>><br>
> # Date 1346084195 -7200<br>
> # Node ID e8e6662faf370bc9209a067cf2d951338bbbe584<br>
> # Parent  99a2a4ae35e2180b7f825ef2677c36d538eac4ba<br>
> obsolete: introduce caches for all meaningful sets<br>
><br>
> This changeset introduces caches on the `obsstore` that keeps track of sets of<br>
> revisions meaningful for obsolescence related logics. For now they are:<br>
><br>
> - obsolete: changesets used as precursors (and not public),<br>
> - extinct:  obsolete changesets with osbolete descendants only,<br>
> - unstable: non obsolete changesets with obsolete ancestors.<br>
><br>
> The cache is accessed using the `getobscache(repo, '<set-name>')` function which<br>
> builds the cache on demand. The `clearobscaches(repo)` function takes care of<br>
> clearing the caches if any.<br>
><br>
> Caches are cleared when one of these events happens:<br>
><br>
> - a new marker is added,<br>
> - a new changeset is added,<br>
> - some changesets are made public,<br>
> - some public changesets are demoted to draft or secret.<br>
><br>
> Declaration of more sets is made easy because we will have to handle at least<br>
> two other "troubles" (latecomer and conflicting).<br>
><br>
> Caches are now used by revset and changectx. It is usually not much more<br>
> expensive to compute the whole set than to check the property of a few elements.<br>
> The performance boost is welcome in case we apply obsolescence logic on a lot of<br>
> revisions. This makes the feature usable!<br>
><br>
> diff --git a/mercurial/context.py b/mercurial/context.py<br>
> --- a/mercurial/context.py<br>
> +++ b/mercurial/context.py<br>
> @@ -9,10 +9,11 @@ from node import nullid, nullrev, short,<br>
>  from i18n import _<br>
>  import ancestor, mdiff, error, util, scmutil, subrepo, patch, encoding, phases<br>
>  import copies<br>
>  import match as matchmod<br>
>  import os, errno, stat<br>
> +import obsolete as obsmod<br>
><br>
>  propertycache = util.propertycache<br>
><br>
>  class changectx(object):<br>
>      """A changecontext object makes access to data related to a particular<br>
> @@ -230,42 +231,27 @@ class changectx(object):<br>
>          for d in self._repo.changelog.descendants([self._rev]):<br>
>              yield changectx(self._repo, d)<br>
><br>
>      def obsolete(self):<br>
>          """True if the changeset is obsolete"""<br>
> -        return (self.node() in self._repo.obsstore.precursors<br>
> -                and self.phase() > phases.public)<br>
> +        return self.rev() in obsmod.getobscache(self._repo, 'obsolete')<br>
><br>
>      def extinct(self):<br>
>          """True if the changeset is extinct"""<br>
>          # We should just compute a cache a check againts it.<br>
>          # see revset implementation for details<br>
>          #<br>
>          # But this naive implementation does not require cache</p>
<p dir="ltr">Looks like this comment should be removed.</p>
<p dir="ltr">> -        if self.phase() <= phases.public:<br>
> -            return False<br>
> -        if not self.obsolete():<br>
> -            return False<br>
> -        for desc in self.descendants():<br>
> -            if not desc.obsolete():<br>
> -                return False<br>
> -        return True<br>
> +        return self.rev() in obsmod.getobscache(self._repo, 'extinct')<br>
><br>
>      def unstable(self):<br>
>          """True if the changeset is not obsolete but it's ancestor are"""<br>
>          # We should just compute /(obsolete()::) - obsolete()/<br>
>          # and keep it in a cache.<br>
>          #<br>
>          # But this naive implementation does not require cache</p>
<p dir="ltr">And this one.</p>
<p dir="ltr">> -        if self.phase() <= phases.public:<br>
> -            return False<br>
> -        if self.obsolete():<br>
> -            return False<br>
> -        for anc in self.ancestors():<br>
> -            if anc.obsolete():<br>
> -                return True<br>
> -        return False<br>
> +        return self.rev() in obsmod.getobscache(self._repo, 'unstable')<br>
><br>
>      def _fileinfo(self, path):<br>
>          if '_manifest' in self.__dict__:<br>
>              try:<br>
>                  return self._manifest[path], self._manifest.flags(path)<br>
> diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py<br>
> --- a/mercurial/localrepo.py<br>
> +++ b/mercurial/localrepo.py<br>
> @@ -1038,10 +1038,11 @@ class localrepository(object):<br>
><br>
>          delcache('_tagscache')<br>
><br>
>          self._branchcache = None # in UTF-8<br>
>          self._branchcachetip = None<br>
> +        obsolete.clearobscaches(self)<br>
><br>
>      def invalidatedirstate(self):<br>
>          '''Invalidates the dirstate, causing the next call to dirstate<br>
>          to check if it was modified since the last time it was read,<br>
>          rereading it if it has.<br>
> @@ -2400,10 +2401,11 @@ class localrepository(object):<br>
>                  htext = _(" (%+d heads)") % dh<br>
><br>
>              self.ui.status(_("added %d changesets"<br>
>                               " with %d changes to %d files%s\n")<br>
>                               % (changesets, revisions, files, htext))<br>
> +            obsolete.clearobscaches(self)<br>
><br>
>              if changesets > 0:<br>
>                  p = lambda: cl.writepending() and self.root or ""<br>
>                  self.hook('pretxnchangegroup', throw=True,<br>
>                            node=hex(cl.node(clstart)), source=srctype,<br>
> diff --git a/mercurial/obsolete.py b/mercurial/obsolete.py<br>
> --- a/mercurial/obsolete.py<br>
> +++ b/mercurial/obsolete.py<br>
> @@ -161,10 +161,12 @@ class obsstore(object):<br>
>      - precursors: old -> set(new)<br>
>      - successors: new -> set(old)<br>
>      """<br>
><br>
>      def __init__(self, sopener):<br>
> +        # caches for various obsolescence related cache<br>
> +        self.caches = {}<br>
>          self._all = []<br>
>          # new markers to serialize<br>
>          self.precursors = {}<br>
>          self.successors = {}<br>
>          self.sopener = sopener<br>
> @@ -220,10 +222,12 @@ class obsstore(object):<br>
>              finally:<br>
>                  # XXX: f.close() == filecache invalidation == obsstore rebuilt.<br>
>                  # call 'filecacheentry.refresh()'  here<br>
>                  f.close()<br>
>              self._load(new)<br>
> +            # new marker *may* have changed several set. invalidate the cache.<br>
> +            self.caches.clear()<br>
>          return len(new)<br>
><br>
>      def mergemarkers(self, transation, data):<br>
>          markers = _readmarkers(data)<br>
>          self.add(transation, markers)<br>
> @@ -327,5 +331,69 @@ def anysuccessors(obsstore, node):<br>
>          for mark in obsstore.precursors.get(current, ()):<br>
>              for suc in mark[1]:<br>
>                  if suc not in seen:<br>
>                      seen.add(suc)<br>
>                      remaining.add(suc)<br>
> +<br>
> +# mapping of 'set-name' -> <function to computer this set><br>
> +cachefuncs = {}<br>
> +def cachefor(name):<br>
> +    """Decorator to register a function as computing the cache for a set"""<br>
> +    def decorator(func):<br>
> +        assert name not in cachefuncs<br>
> +        cachefuncs[name] = func<br>
> +        return func<br>
> +    return decorator<br>
> +<br>
> +def getobscache(repo, name):<br>
> +    """Return the set of revision that belong to the <name> set<br>
> +<br>
> +    Such access may compute the set and cache it for future use"""<br>
> +    if not repo.obsstore:<br>
> +        return ()<br>
> +    if name not in repo.obsstore.caches:<br>
> +        repo.obsstore.caches[name] = cachefuncs[name](repo)<br>
> +    return repo.obsstore.caches[name]<br>
> +<br>
> +# To be simple we need to invalidate obsolescence cache when:<br>
> +#<br>
> +# - new changeset is added:<br>
> +# - public phase is changed<br>
> +# - obsolescence marker are added<br>
> +# - strip is used a repo<br>
> +def clearobscaches(repo):<br>
> +    """Remove all obsolescence related cache from a repo<br>
> +<br>
> +    This remove all cache in obsstore is the obsstore already exist on the<br>
> +    repo.<br>
> +<br>
> +    (We could be smarter here given the exact event that trigger the cache<br>
> +    clearing)"""<br>
> +    # only clear cache is there is obsstore data in this repo<br>
> +    if 'obsstore' in repo._filecache:<br>
> +        repo.obsstore.caches.clear()<br>
> +<br>
> +@cachefor('obsolete')<br>
> +def _computeobsoleteset(repo):<br>
> +    """the set of obsolete revisions"""<br>
> +    obs = set()<br>
> +    nm = repo.changelog.nodemap<br>
> +    for prec in repo.obsstore.precursors:<br>
> +        rev = nm.get(prec)<br>
> +        if rev is not None:<br>
> +            obs.add(rev)<br>
> +    return set(repo.revs('%ld - public()', obs))<br>
> +<br>
> +@cachefor('unstable')<br>
> +def _computeunstableset(repo):<br>
> +    """the set of non obsolete revisions with obsolete parents"""<br>
> +    return set(repo.revs('(obsolete()::) - obsolete()'))<br>
> +<br>
> +@cachefor('suspended')<br>
> +def _computesuspendedset(repo):<br>
> +    """the set of obsolete parents with non obsolete descendants"""<br>
> +    return set(repo.revs('obsolete() and obsolete()::unstable()'))<br>
> +<br>
> +@cachefor('extinct')<br>
> +def _computeextinctset(repo):<br>
> +    """the set of obsolete parents without non obsolete descendants"""<br>
> +    return set(repo.revs('obsolete() - obsolete()::unstable()'))<br>
> diff --git a/mercurial/phases.py b/mercurial/phases.py<br>
> --- a/mercurial/phases.py<br>
> +++ b/mercurial/phases.py<br>
> @@ -102,10 +102,11 @@ Note: old client behave as a publishing<br>
><br>
>  import errno<br>
>  from node import nullid, nullrev, bin, hex, short<br>
>  from i18n import _<br>
>  import util<br>
> +import obsolete<br>
><br>
>  allphases = public, draft, secret = range(3)<br>
>  trackedphases = allphases[1:]<br>
>  phasenames = ['public', 'draft', 'secret']<br>
><br>
> @@ -242,10 +243,11 @@ class phasecache(object):<br>
>                  # some roots may need to be declared for lower phases<br>
>                  delroots.extend(olds - roots)<br>
>              # declare deleted root in the target phase<br>
>              if targetphase != 0:<br>
>                  self.retractboundary(repo, targetphase, delroots)<br>
> +        obsolete.clearobscaches(repo)<br>
><br>
>      def retractboundary(self, repo, targetphase, nodes):<br>
>          # Be careful to preserve shallow-copied values: do not update<br>
>          # phaseroots values, replace them.<br>
><br>
> @@ -258,10 +260,11 @@ class phasecache(object):<br>
>              currentroots = currentroots.copy()<br>
>              currentroots.update(newroots)<br>
>              ctxs = repo.set('roots(%ln::)', currentroots)<br>
>              currentroots.intersection_update(ctx.node() for ctx in ctxs)<br>
>              self._updateroots(targetphase, currentroots)<br>
> +        obsolete.clearobscaches(repo)<br>
><br>
>  def advanceboundary(repo, targetphase, nodes):<br>
>      """Add nodes to a phase changing other nodes phases if necessary.<br>
><br>
>      This function move boundary *forward* this means that all nodes<br>
> diff --git a/mercurial/revset.py b/mercurial/revset.py<br>
> --- a/mercurial/revset.py<br>
> +++ b/mercurial/revset.py<br>
> @@ -10,10 +10,11 @@ import parser, util, error, discovery, h<br>
>  import node<br>
>  import bookmarks as bookmarksmod<br>
>  import match as matchmod<br>
>  from i18n import _<br>
>  import encoding<br>
> +import obsolete as obsmod<br>
><br>
>  def _revancestors(repo, revs, followfirst):<br>
>      """Like revlog.ancestors(), but supports followfirst."""<br>
>      cut = followfirst and 1 or None<br>
>      cl = repo.changelog<br>
> @@ -619,12 +620,12 @@ def extinct(repo, subset, x):<br>
>      """``extinct()``<br>
>      Obsolete changesets with obsolete descendants only.<br>
>      """<br>
>      # i18n: "extinct" is a keyword<br>
>      getargs(x, 0, 0, _("extinct takes no arguments"))<br>
> -    extinctset = set(repo.revs('(obsolete()::) - (::(not obsolete()))'))<br>
> -    return [r for r in subset if r in extinctset]<br>
> +    extincts = obsmod.getobscache(repo, 'extinct')<br>
> +    return [r for r in subset if r in extincts]<br>
><br>
>  def extra(repo, subset, x):<br>
>      """``extra(label, [value])``<br>
>      Changesets with the given label in the extra metadata, with the given<br>
>      optional value.<br>
> @@ -957,11 +958,12 @@ def node_(repo, subset, x):<br>
>  def obsolete(repo, subset, x):<br>
>      """``obsolete()``<br>
>      Mutable changeset with a newer version."""<br>
>      # i18n: "obsolete" is a keyword<br>
>      getargs(x, 0, 0, _("obsolete takes no arguments"))<br>
> -    return [r for r in subset if repo[r].obsolete()]<br>
> +    obsoletes = obsmod.getobscache(repo, 'obsolete')<br>
> +    return [r for r in subset if r in obsoletes]<br>
><br>
>  def origin(repo, subset, x):<br>
>      """``origin([set])``<br>
>      Changesets that were specified as a source for the grafts, transplants or<br>
>      rebases that created the given revisions.  Omitting the optional set is the<br>
> @@ -1435,12 +1437,12 @@ def unstable(repo, subset, x):<br>
>      """``unstable()``<br>
>      Non-obsolete changesets with obsolete ancestors.<br>
>      """<br>
>      # i18n: "unstable" is a keyword<br>
>      getargs(x, 0, 0, _("unstable takes no arguments"))<br>
> -    unstableset = set(repo.revs('(obsolete()::) - obsolete()'))<br>
> -    return [r for r in subset if r in unstableset]<br>
> +    unstables = obsmod.getobscache(repo, 'unstable')<br>
> +    return [r for r in subset if r in unstables]<br>
><br>
><br>
>  def user(repo, subset, x):<br>
>      """``user(string)``<br>
>      User name contains string. The match is case-insensitive.<br>
> _______________________________________________<br>
> Mercurial-devel mailing list<br>
> <a href="mailto:Mercurial-devel@selenic.com">Mercurial-devel@selenic.com</a><br>
> <a href="http://selenic.com/mailman/listinfo/mercurial-devel">http://selenic.com/mailman/listinfo/mercurial-devel</a><br>
</p>