# HG changeset patch # User Pierre-Yves David # Date 1511359515 -3600 # Node ID 9361149224a766edf1e8c1917041e4b6e3face1d # Parent 0c8c7b5274a50636311ccc406e97310b3d6314e7 depthcache: move to a dedicated object and storage The SQL cache was overkill and fragile. We move to a dedicated storage using the new generic class. diff -r 0c8c7b5274a5 -r 9361149224a7 hgext3rd/evolve/depthcache.py --- a/hgext3rd/evolve/depthcache.py Wed Nov 22 13:44:44 2017 +0100 +++ b/hgext3rd/evolve/depthcache.py Wed Nov 22 15:05:15 2017 +0100 @@ -1,11 +1,19 @@ from __future__ import absolute_import +import array +import weakref + from mercurial import ( + localrepo, + node as nodemod, + util, scmutil, ) from . import ( + error, exthelper, + genericcaches, ) from mercurial.i18n import _ @@ -20,12 +28,186 @@ 'debugdepth', [ ('r', 'rev', [], 'revs to print depth for'), + ('', 'method', 'cached', "one of 'simple', 'cached', 'compare'"), ], _('REVS')) def debugdepth(ui, repo, **opts): """display depth of REVS """ revs = scmutil.revrange(repo, opts['rev']) + method = opts['method'] + if method == 'cached': + cache = repo.depthcache + cache.save(repo) for r in revs: ctx = repo[r] - ui.write('%s %d\n' % (ctx, simpledepth(repo, r))) + if method == 'simple': + depth = simpledepth(repo, r) + elif method == 'cached': + depth = cache.get(r) + elif method == 'compare': + simple = simpledepth(repo, r) + cached = cache.get(r) + if simple != cached: + raise error.Abort('depth differ for revision %s: %d != %d' + % (ctx, simple, cached)) + else: + raise error.Abort('unknown method "%s"' % method) + ui.write('%s %d\n' % (ctx, depth)) + +@eh.reposetup +def setupcache(ui, repo): + + class depthcacherepo(repo.__class__): + + @localrepo.unfilteredpropertycache + def depthcache(self): + cache = depthcache() + cache.update(self) + return cache + + @localrepo.unfilteredmethod + def destroyed(self): + if 'obsstore' in vars(self): + self.depthcache.clear() + super(depthcacherepo, self).destroyed() + + if util.safehasattr(repo, 'updatecaches'): + @localrepo.unfilteredmethod + def updatecaches(self, tr=None): + if (repo.ui.configbool('experimental', 'obshashrange', + False) + and repo.ui.configbool('experimental', + 'obshashrange.warm-cache', + True)): + self.depthcache.update(repo) + self.depthcache.save(repo) + super(depthcacherepo, self).updatecaches(tr) + + else: + def transaction(self, *args, **kwargs): + tr = super(depthcacherepo, self).transaction(*args, **kwargs) + reporef = weakref.ref(self) + + def _warmcache(tr): + repo = reporef() + if repo is None: + return + repo = repo.unfiltered() + # As pointed in 'obscache.update', we could have the changelog + # and the obsstore in charge of updating the cache when new + # items goes it. The tranaction logic would then only be + # involved for the 'pending' and final writing on disk. + self.obsstore.obscache.update(repo) + self.obsstore.obscache.save(repo) + + if (repo.ui.configbool('experimental', 'obshashrange', + False) + and repo.ui.configbool('experimental', + 'obshashrange.warm-cache', + True)): + tr.addpostclose('warmcache-depthcache', _warmcache) + return tr + + repo.__class__ = depthcacherepo + +class depthcache(genericcaches.changelogsourcebase): + + _filepath = 'evoext-depthcache-00' + _cachename = 'evo-ext-depthcache' + + def __init__(self): + super(depthcache, self).__init__() + self._data = array.array('l') + + def get(self, rev): + if len(self._data) <= rev: + raise error.ProgrammingError('depthcache must be warmed before use') + return self._data[rev] + + def _updatefrom(self, repo, data): + """compute the rev of one revision, assert previous revision has an hot cache + """ + cl = repo.unfiltered().changelog + for rev in data: + assert rev == len(self._data), (rev, len(self._data)) + self._data.append(self._depth(cl, rev)) + + def _depth(self, changelog, rev): + cl = changelog + p1, p2 = cl.parentrevs(rev) + if p1 == nodemod.nullrev: + # root case + return 1 + elif p2 == nodemod.nullrev: + # linear commit case + return self.get(p1) + 1 + # merge case, must find the amount of exclusive content + depth_p1 = self.get(p1) + depth_p2 = self.get(p2) + # computing depth of a merge + ancnodes = cl.commonancestorsheads(cl.node(p1), cl.node(p2)) + if not ancnodes: + # unrelated branch, (no common root) + revdepth = depth_p1 + depth_p2 + 1 + elif len(ancnodes) == 1: + # one unique branch point: + # we can compute depth without any walk + ancrev = cl.rev(ancnodes[0]) + depth_anc = self.get(ancrev) + revdepth = depth_p1 + (depth_p2 - depth_anc) + 1 + else: + # we pick the parent that is that is + # * the deepest (less changeset outside of it), + # * lowest revs because more chance to have descendant of other "above" + parents = [(p1, depth_p1), (p2, depth_p2)] + parents.sort(key=lambda x: (x[1], -x[0])) + revdepth = parents[1][1] + revdepth += len(cl.findmissingrevs(common=[parents[1][0]], + heads=[parents[0][0]])) + revdepth += 1 # the merge revision + return revdepth + + # cache internal logic + + def clear(self, reset=False): + """invalidate the cache content + + if 'reset' is passed, we detected a strip and the cache will have to be + recomputed. + + Subclasses MUST overide this method to actually affect the cache data. + """ + super(depthcache, self).clear() + self._data = array.array('l') + + # crude version of a cache, to show the kind of information we have to store + + def load(self, repo): + """load data from disk""" + assert repo.filtername is None + + data = repo.cachevfs.tryread(self._filepath) + self._data = array.array('l') + if not data: + self._cachekey = self.emptykey + else: + headerdata = data[:self._cachekeysize] + self._cachekey = self._deserializecachekey(headerdata) + self._data.fromstring(data[self._cachekeysize:]) + self._ondiskkey = self._cachekey + + def save(self, repo): + """save the data to disk + + Format is pretty simple, we serialise the cache key and then drop the + bytearray. + """ + if self._cachekey is None or self._cachekey == self._ondiskkey: + return + + cachefile = repo.cachevfs(self._filepath, 'w', atomictemp=True) + headerdata = self._serializecachekey() + cachefile.write(headerdata) + cachefile.write(self._data.tostring()) + cachefile.close() diff -r 0c8c7b5274a5 -r 9361149224a7 hgext3rd/evolve/stablerange.py --- a/hgext3rd/evolve/stablerange.py Wed Nov 22 13:44:44 2017 +0100 +++ b/hgext3rd/evolve/stablerange.py Wed Nov 22 15:05:15 2017 +0100 @@ -249,8 +249,6 @@ # The point up to which we have data in cache self._tiprev = None self._tipnode = None - # cache the 'depth' of a changeset, the size of '::rev' - self._depthcache = {} # cache the standard stable subranges or a range self._subrangescache = {} # To slices merge, we need to walk their descendant in reverse stable @@ -273,6 +271,7 @@ def warmup(self, repo, upto=None): """warm the cache up""" repo = repo.unfiltered() + repo.depthcache.update(repo) cl = repo.changelog # subrange should be warmed from head to range to be able to benefit # from revsfromrange cache. otherwise each merge will trigger its own @@ -340,38 +339,7 @@ duration) def depthrev(self, repo, rev): - repo = repo.unfiltered() - cl = repo.changelog - depth = self._getdepth - nullrev = nodemod.nullrev - stack = [rev] - while stack: - revdepth = None - current = stack[-1] - revdepth = depth(current) - if revdepth is not None: - stack.pop() - continue - p1, p2 = self._parents(current, cl.parentrevs) - if p1 == nullrev: - # root case - revdepth = 1 - elif p2 == nullrev: - # linear commit case - parentdepth = depth(p1) - if parentdepth is None: - stack.append(p1) - else: - revdepth = parentdepth + 1 - else: - # merge case - revdepth = self._depthmerge(cl, current, p1, p2, stack) - if revdepth is not None: - self._setdepth(current, revdepth) - stack.pop() - # actual_depth = len(list(cl.ancestors([rev], inclusive=True))) - # assert revdepth == actual_depth, (rev, revdepth, actual_depth) - return revdepth + return repo.depthcache.get(rev) def rangelength(self, repo, rangeid): headrev, index = rangeid[0], rangeid[1] @@ -417,18 +385,6 @@ self._parentscache[rev] = parents return parents - def _getdepth(self, rev): - """utility function used to access the depth cache - - This mostly exist to help the on disk persistence.""" - return self._depthcache.get(rev) - - def _setdepth(self, rev, value): - """utility function used to set the depth cache - - This mostly exist to help the on disk persistence.""" - self._depthcache[rev] = value - def _getsub(self, rev): """utility function used to access the subranges cache @@ -491,63 +447,18 @@ expected = len(revs) - 1 # Since we do warmup properly, we can expect the cache to be hot # for everythin under the merge we investigate - cache = self._depthcache + cache = repo.depthcache # note: we cannot do a binary search because element under the # inherited point might have mismatching depth because of inner # branching. for rev in i: - if cache[rev] == expected: + if cache.get(rev) == expected: break expected -= 1 value = (expected - 1, rev) self._inheritancecache[merge] = value return value - def _depthmerge(self, cl, rev, p1, p2, stack): - # sub method to simplify the main 'depthrev' one - revdepth = None - depth = self._getdepth - depth_p1 = depth(p1) - depth_p2 = depth(p2) - missingparent = False - if depth_p1 is None: - stack.append(p1) - missingparent = True - if depth_p2 is None: - stack.append(p2) - missingparent = True - if missingparent: - return None - # computin depth of a merge - # XXX the common ancestors heads could be cached - ancnodes = cl.commonancestorsheads(cl.node(p1), cl.node(p2)) - ancrevs = [cl.rev(a) for a in ancnodes] - anyunkown = False - ancdepth = [] - for r in ancrevs: - d = depth(r) - if d is None: - anyunkown = True - stack.append(r) - ancdepth.append((r, d)) - if anyunkown: - return None - if not ancrevs: - # unrelated branch, (no common root) - revdepth = depth_p1 + depth_p2 + 1 - elif len(ancrevs) == 1: - # one unique branch point: - # we can compute depth without any walk - depth_anc = ancdepth[0][1] - revdepth = depth_p1 + (depth_p2 - depth_anc) + 1 - else: - # multiple ancestors, we pick one that is - # * the deepest (less changeset outside of it), - # * lowest revs because more chance to have descendant of other "above" - anc, revdepth = max(ancdepth, key=lambda x: (x[1], -x[0])) - revdepth += len(cl.findmissingrevs(common=[anc], heads=[rev])) - return revdepth - def _subranges(self, repo, rangeid): if self.rangelength(repo, rangeid) == 1: return [] @@ -706,7 +617,6 @@ tiprev INTEGER NOT NULL, tipnode BLOB NOT NULL );""", - "CREATE TABLE depth(rev INTEGER NOT NULL PRIMARY KEY, depth INTEGER NOT NULL);", """CREATE TABLE range(rev INTEGER NOT NULL, idx INTEGER NOT NULL, PRIMARY KEY(rev, idx));""", @@ -721,19 +631,15 @@ );""", "CREATE INDEX subranges_index ON subranges (suprev, supidx);", "CREATE INDEX range_index ON range (rev, idx);", - "CREATE INDEX depth_index ON depth (rev);" ] _newmeta = "INSERT INTO meta (schemaversion, tiprev, tipnode) VALUES (?,?,?);" _updatemeta = "UPDATE meta SET tiprev = ?, tipnode = ?;" -_updatedepth = "INSERT INTO depth(rev, depth) VALUES (?,?);" _updaterange = "INSERT INTO range(rev, idx) VALUES (?,?);" _updatesubranges = """INSERT INTO subranges(listidx, suprev, supidx, subrev, subidx) VALUES (?,?,?,?,?);""" _queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';" _querymeta = "SELECT schemaversion, tiprev, tipnode FROM meta;" -_querydepth = "SELECT depth FROM depth WHERE rev = ?;" -_batchdepth = "SELECT rev, depth FROM depth;" _queryrange = "SELECT * FROM range WHERE (rev = ? AND idx = ?);" _querysubranges = """SELECT subrev, subidx FROM subranges @@ -742,7 +648,7 @@ class sqlstablerange(stablerange): - _schemaversion = 0 + _schemaversion = 1 def __init__(self, repo): lrusize = repo.ui.configint('experimental', 'obshashrange.lru-size', @@ -753,9 +659,7 @@ self._cl = repo.unfiltered().changelog # (okay to keep an old one) self._ondisktiprev = None self._ondisktipnode = None - self._unsaveddepth = {} self._unsavedsubranges = {} - self._fulldepth = False def warmup(self, repo, upto=None): self._con # make sure the data base is loaded @@ -776,22 +680,6 @@ repo.ui.warn('(cache will not be saved)\n') super(sqlstablerange, self).warmup(repo, upto) - def _getdepth(self, rev): - cache = self._depthcache - if rev not in cache and rev <= self._ondisktiprev and self._con is not None: - value = None - result = self._con.execute(_querydepth, (rev,)).fetchone() - if result is not None: - value = result[0] - # in memory caching of the value - cache[rev] = value - return cache.get(rev) - - def _setdepth(self, rev, depth): - assert rev not in self._unsaveddepth - self._unsaveddepth[rev] = depth - super(sqlstablerange, self)._setdepth(rev, depth) - def _getsub(self, rangeid): cache = self._subrangescache if rangeid not in cache and rangeid[0] <= self._ondisktiprev and self._con is not None: @@ -808,10 +696,6 @@ self._unsavedsubranges[rangeid] = value super(sqlstablerange, self)._setsub(rangeid, value) - def _inheritancepoint(self, *args, **kwargs): - self._loaddepth() - return super(sqlstablerange, self)._inheritancepoint(*args, **kwargs) - def _db(self): try: util.makedirs(self._vfs.dirname(self._path)) @@ -847,7 +731,8 @@ def _save(self, repo): repo = repo.unfiltered() - if not (self._unsavedsubranges or self._unsaveddepth): + repo.depthcache.save(repo) + if not self._unsavedsubranges: return # no new data if self._con is None: @@ -886,26 +771,12 @@ ] con.execute(_updatemeta, meta) - self._savedepth(con, repo) self._saverange(con, repo) con.commit() self._ondisktiprev = self._tiprev self._ondisktipnode = self._tipnode - self._unsaveddepth.clear() self._unsavedsubranges.clear() - def _savedepth(self, con, repo): - repo = repo.unfiltered() - data = self._unsaveddepth.items() - con.executemany(_updatedepth, data) - - def _loaddepth(self): - """batch load all data about depth""" - if not (self._fulldepth or self._con is None): - result = self._con.execute(_batchdepth) - self._depthcache.update(result.fetchall()) - self._fulldepth = True - def _saverange(self, con, repo): repo = repo.unfiltered() data = [] diff -r 0c8c7b5274a5 -r 9361149224a7 tests/test-discovery-obshashrange.t --- a/tests/test-discovery-obshashrange.t Wed Nov 22 13:44:44 2017 +0100 +++ b/tests/test-discovery-obshashrange.t Wed Nov 22 15:05:15 2017 +0100 @@ -35,6 +35,8 @@ * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio (glob) * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob) * @0000000000000000000000000000000000000000 (*)> debugbuilddag .+7 (glob) + * @0000000000000000000000000000000000000000 (*)> strip detected, evo-ext-depthcache cache reset (glob) + * @0000000000000000000000000000000000000000 (*)> updated evo-ext-depthcache in *.???? seconds (8r) (glob) * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (8r, 0o) (glob) * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob) * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob) @@ -155,6 +157,8 @@ $ hg blackbox * @0000000000000000000000000000000000000000 (*)> clone 'ssh://user@dummy/server' client exited 0 after *.?? seconds (glob) * @0000000000000000000000000000000000000000 (*)> pull --rev 4 (glob) + * @0000000000000000000000000000000000000000 (*)> strip detected, evo-ext-depthcache cache reset (glob) + * @0000000000000000000000000000000000000000 (*)> updated evo-ext-depthcache in *.???? seconds (5r) (glob) * @0000000000000000000000000000000000000000 (*)> updated stablerange cache in *.???? seconds (glob) * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (5r, 3o) (glob) * @0000000000000000000000000000000000000000 (*)> updated base branch cache in *.???? seconds (glob) @@ -236,6 +240,7 @@ received listkey for "phases": 58 bytes $ hg -R ../server blackbox * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio (glob) + * @0000000000000000000000000000000000000000 (*)> updated evo-ext-depthcache in *.???? seconds (1r) (glob) * @0000000000000000000000000000000000000000 (*)> updated stablerange cache in *.???? seconds (glob) * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob) * @0000000000000000000000000000000000000000 (*)> obscache is out of date, falling back to slower obsstore version (glob) @@ -298,6 +303,7 @@ * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> add foo (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> add foo exited 0 after *.?? seconds (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> commit -m foo (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-depthcache in *.???? seconds (1r) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 0o) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obscache is out of date, falling back to slower obsstore version (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated served branch cache in *.???? seconds (glob) @@ -412,6 +418,7 @@ * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G exited 0 after *.?? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r 6 (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 2/6 mismatch - 1 obshashrange queries in *.???? seconds (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-depthcache in *.???? seconds (2r) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (2r, 3o) (glob) @@ -558,6 +565,7 @@ * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev 'heads(all())' exited 0 after *.?? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 0/8 mismatch - 1 obshashrange queries in *.???? seconds (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-depthcache in *.???? seconds (1r) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated served branch cache in *.???? seconds (glob) @@ -566,6 +574,8 @@ * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> 1 incoming changes - new heads: 4de32a90b66c (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull exited 0 after *.?? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> rollback (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> strip detected, evo-ext-depthcache cache reset (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-depthcache in *.???? seconds (8r) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated base branch cache in *.???? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> wrote base branch cache with 1 labels and 2 nodes (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> strip detected, evo-ext-obscache cache reset (glob) @@ -610,6 +620,7 @@ * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev 'heads(all())' exited 0 after *.?? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 0/8 mismatch - 1 obshashrange queries in *.???? seconds (glob) + * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-depthcache in *.???? seconds (1r) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob) * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (1r, 1o) (glob) @@ -722,9 +733,12 @@ * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> --config 'extensions.strip=' strip -r 'desc("foo")' (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> saved backup bundle to $TESTTMP/client/.hg/strip-backup/45f8b879de92-94c82517-backup.hg (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> strip detected, evo-ext-obshashrange cache reset (glob) + * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> strip detected, evo-ext-depthcache cache reset (glob) + * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-depthcache in *.???? seconds (5r) (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (5r, 11o) (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> strip detected, evo-ext-obscache cache reset (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (5r, 11o) (glob) + * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-depthcache in *.???? seconds (3r) (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated stablerange cache in *.???? seconds (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (3r, 0o) (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (3r, 0o) (glob) @@ -737,6 +751,7 @@ * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> log -G exited 0 after *.?? seconds (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> pull (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> obsdiscovery, 1/8 mismatch - 1 obshashrange queries in *.???? seconds (glob) + * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-depthcache in *.???? seconds (1r) (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated stablerange cache in *.???? seconds (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> obshashcache reset - new markers affect cached ranges (glob) * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 2o) (glob)