##// END OF EJS Templates
contributor: change mentions of mpm to olivia...
contributor: change mentions of mpm to olivia Matt Mackall is now Olivia Mackall. I reached out to her about changing the copyright notices to reflect this change and she gave me the green light, so I changed everything relevant. Differential Revision: https://phab.mercurial-scm.org/D10266

File last commit:

r47575:d4ba4d51 default
r47575:d4ba4d51 default
Show More
deltas.py
1088 lines | 36.7 KiB | text/x-python | PythonLexer
# revlogdeltas.py - Logic around delta computation for revlog
#
# Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
# Copyright 2018 Octobus <contact@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""Helper class to compute deltas stored inside revlogs"""
from __future__ import absolute_import
import collections
import struct
# import stuff from node for others to import from revlog
from ..node import nullrev
from ..i18n import _
from ..pycompat import getattr
from .constants import (
REVIDX_ISCENSORED,
REVIDX_RAWTEXT_CHANGING_FLAGS,
)
from ..thirdparty import attr
from .. import (
error,
mdiff,
util,
)
from . import flagutil
# maximum <delta-chain-data>/<revision-text-length> ratio
LIMIT_DELTA2TEXT = 2
class _testrevlog(object):
"""minimalist fake revlog to use in doctests"""
def __init__(self, data, density=0.5, mingap=0, snapshot=()):
"""data is an list of revision payload boundaries"""
self._data = data
self._srdensitythreshold = density
self._srmingapsize = mingap
self._snapshot = set(snapshot)
self.index = None
def start(self, rev):
if rev == nullrev:
return 0
if rev == 0:
return 0
return self._data[rev - 1]
def end(self, rev):
if rev == nullrev:
return 0
return self._data[rev]
def length(self, rev):
return self.end(rev) - self.start(rev)
def __len__(self):
return len(self._data)
def issnapshot(self, rev):
if rev == nullrev:
return True
return rev in self._snapshot
def slicechunk(revlog, revs, targetsize=None):
"""slice revs to reduce the amount of unrelated data to be read from disk.
``revs`` is sliced into groups that should be read in one time.
Assume that revs are sorted.
The initial chunk is sliced until the overall density (payload/chunks-span
ratio) is above `revlog._srdensitythreshold`. No gap smaller than
`revlog._srmingapsize` is skipped.
If `targetsize` is set, no chunk larger than `targetsize` will be yield.
For consistency with other slicing choice, this limit won't go lower than
`revlog._srmingapsize`.
If individual revisions chunk are larger than this limit, they will still
be raised individually.
>>> data = [
... 5, #00 (5)
... 10, #01 (5)
... 12, #02 (2)
... 12, #03 (empty)
... 27, #04 (15)
... 31, #05 (4)
... 31, #06 (empty)
... 42, #07 (11)
... 47, #08 (5)
... 47, #09 (empty)
... 48, #10 (1)
... 51, #11 (3)
... 74, #12 (23)
... 85, #13 (11)
... 86, #14 (1)
... 91, #15 (5)
... ]
>>> revlog = _testrevlog(data, snapshot=range(16))
>>> list(slicechunk(revlog, list(range(16))))
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
>>> list(slicechunk(revlog, [0, 15]))
[[0], [15]]
>>> list(slicechunk(revlog, [0, 11, 15]))
[[0], [11], [15]]
>>> list(slicechunk(revlog, [0, 11, 13, 15]))
[[0], [11, 13, 15]]
>>> list(slicechunk(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
[[1, 2], [5, 8, 10, 11], [14]]
Slicing with a maximum chunk size
>>> list(slicechunk(revlog, [0, 11, 13, 15], targetsize=15))
[[0], [11], [13], [15]]
>>> list(slicechunk(revlog, [0, 11, 13, 15], targetsize=20))
[[0], [11], [13, 15]]
Slicing involving nullrev
>>> list(slicechunk(revlog, [-1, 0, 11, 13, 15], targetsize=20))
[[-1, 0], [11], [13, 15]]
>>> list(slicechunk(revlog, [-1, 13, 15], targetsize=5))
[[-1], [13], [15]]
"""
if targetsize is not None:
targetsize = max(targetsize, revlog._srmingapsize)
# targetsize should not be specified when evaluating delta candidates:
# * targetsize is used to ensure we stay within specification when reading,
densityslicing = getattr(revlog.index, 'slicechunktodensity', None)
if densityslicing is None:
densityslicing = lambda x, y, z: _slicechunktodensity(revlog, x, y, z)
for chunk in densityslicing(
revs, revlog._srdensitythreshold, revlog._srmingapsize
):
for subchunk in _slicechunktosize(revlog, chunk, targetsize):
yield subchunk
def _slicechunktosize(revlog, revs, targetsize=None):
"""slice revs to match the target size
This is intended to be used on chunk that density slicing selected by that
are still too large compared to the read garantee of revlog. This might
happens when "minimal gap size" interrupted the slicing or when chain are
built in a way that create large blocks next to each other.
>>> data = [
... 3, #0 (3)
... 5, #1 (2)
... 6, #2 (1)
... 8, #3 (2)
... 8, #4 (empty)
... 11, #5 (3)
... 12, #6 (1)
... 13, #7 (1)
... 14, #8 (1)
... ]
== All snapshots cases ==
>>> revlog = _testrevlog(data, snapshot=range(9))
Cases where chunk is already small enough
>>> list(_slicechunktosize(revlog, [0], 3))
[[0]]
>>> list(_slicechunktosize(revlog, [6, 7], 3))
[[6, 7]]
>>> list(_slicechunktosize(revlog, [0], None))
[[0]]
>>> list(_slicechunktosize(revlog, [6, 7], None))
[[6, 7]]
cases where we need actual slicing
>>> list(_slicechunktosize(revlog, [0, 1], 3))
[[0], [1]]
>>> list(_slicechunktosize(revlog, [1, 3], 3))
[[1], [3]]
>>> list(_slicechunktosize(revlog, [1, 2, 3], 3))
[[1, 2], [3]]
>>> list(_slicechunktosize(revlog, [3, 5], 3))
[[3], [5]]
>>> list(_slicechunktosize(revlog, [3, 4, 5], 3))
[[3], [5]]
>>> list(_slicechunktosize(revlog, [5, 6, 7, 8], 3))
[[5], [6, 7, 8]]
>>> list(_slicechunktosize(revlog, [0, 1, 2, 3, 4, 5, 6, 7, 8], 3))
[[0], [1, 2], [3], [5], [6, 7, 8]]
Case with too large individual chunk (must return valid chunk)
>>> list(_slicechunktosize(revlog, [0, 1], 2))
[[0], [1]]
>>> list(_slicechunktosize(revlog, [1, 3], 1))
[[1], [3]]
>>> list(_slicechunktosize(revlog, [3, 4, 5], 2))
[[3], [5]]
== No Snapshot cases ==
>>> revlog = _testrevlog(data)
Cases where chunk is already small enough
>>> list(_slicechunktosize(revlog, [0], 3))
[[0]]
>>> list(_slicechunktosize(revlog, [6, 7], 3))
[[6, 7]]
>>> list(_slicechunktosize(revlog, [0], None))
[[0]]
>>> list(_slicechunktosize(revlog, [6, 7], None))
[[6, 7]]
cases where we need actual slicing
>>> list(_slicechunktosize(revlog, [0, 1], 3))
[[0], [1]]
>>> list(_slicechunktosize(revlog, [1, 3], 3))
[[1], [3]]
>>> list(_slicechunktosize(revlog, [1, 2, 3], 3))
[[1], [2, 3]]
>>> list(_slicechunktosize(revlog, [3, 5], 3))
[[3], [5]]
>>> list(_slicechunktosize(revlog, [3, 4, 5], 3))
[[3], [4, 5]]
>>> list(_slicechunktosize(revlog, [5, 6, 7, 8], 3))
[[5], [6, 7, 8]]
>>> list(_slicechunktosize(revlog, [0, 1, 2, 3, 4, 5, 6, 7, 8], 3))
[[0], [1, 2], [3], [5], [6, 7, 8]]
Case with too large individual chunk (must return valid chunk)
>>> list(_slicechunktosize(revlog, [0, 1], 2))
[[0], [1]]
>>> list(_slicechunktosize(revlog, [1, 3], 1))
[[1], [3]]
>>> list(_slicechunktosize(revlog, [3, 4, 5], 2))
[[3], [5]]
== mixed case ==
>>> revlog = _testrevlog(data, snapshot=[0, 1, 2])
>>> list(_slicechunktosize(revlog, list(range(9)), 5))
[[0, 1], [2], [3, 4, 5], [6, 7, 8]]
"""
assert targetsize is None or 0 <= targetsize
startdata = revlog.start(revs[0])
enddata = revlog.end(revs[-1])
fullspan = enddata - startdata
if targetsize is None or fullspan <= targetsize:
yield revs
return
startrevidx = 0
endrevidx = 1
iterrevs = enumerate(revs)
next(iterrevs) # skip first rev.
# first step: get snapshots out of the way
for idx, r in iterrevs:
span = revlog.end(r) - startdata
snapshot = revlog.issnapshot(r)
if span <= targetsize and snapshot:
endrevidx = idx + 1
else:
chunk = _trimchunk(revlog, revs, startrevidx, endrevidx)
if chunk:
yield chunk
startrevidx = idx
startdata = revlog.start(r)
endrevidx = idx + 1
if not snapshot:
break
# for the others, we use binary slicing to quickly converge toward valid
# chunks (otherwise, we might end up looking for start/end of many
# revisions). This logic is not looking for the perfect slicing point, it
# focuses on quickly converging toward valid chunks.
nbitem = len(revs)
while (enddata - startdata) > targetsize:
endrevidx = nbitem
if nbitem - startrevidx <= 1:
break # protect against individual chunk larger than limit
localenddata = revlog.end(revs[endrevidx - 1])
span = localenddata - startdata
while span > targetsize:
if endrevidx - startrevidx <= 1:
break # protect against individual chunk larger than limit
endrevidx -= (endrevidx - startrevidx) // 2
localenddata = revlog.end(revs[endrevidx - 1])
span = localenddata - startdata
chunk = _trimchunk(revlog, revs, startrevidx, endrevidx)
if chunk:
yield chunk
startrevidx = endrevidx
startdata = revlog.start(revs[startrevidx])
chunk = _trimchunk(revlog, revs, startrevidx)
if chunk:
yield chunk
def _slicechunktodensity(revlog, revs, targetdensity=0.5, mingapsize=0):
"""slice revs to reduce the amount of unrelated data to be read from disk.
``revs`` is sliced into groups that should be read in one time.
Assume that revs are sorted.
The initial chunk is sliced until the overall density (payload/chunks-span
ratio) is above `targetdensity`. No gap smaller than `mingapsize` is
skipped.
>>> revlog = _testrevlog([
... 5, #00 (5)
... 10, #01 (5)
... 12, #02 (2)
... 12, #03 (empty)
... 27, #04 (15)
... 31, #05 (4)
... 31, #06 (empty)
... 42, #07 (11)
... 47, #08 (5)
... 47, #09 (empty)
... 48, #10 (1)
... 51, #11 (3)
... 74, #12 (23)
... 85, #13 (11)
... 86, #14 (1)
... 91, #15 (5)
... ])
>>> list(_slicechunktodensity(revlog, list(range(16))))
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]]
>>> list(_slicechunktodensity(revlog, [0, 15]))
[[0], [15]]
>>> list(_slicechunktodensity(revlog, [0, 11, 15]))
[[0], [11], [15]]
>>> list(_slicechunktodensity(revlog, [0, 11, 13, 15]))
[[0], [11, 13, 15]]
>>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14]))
[[1, 2], [5, 8, 10, 11], [14]]
>>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
... mingapsize=20))
[[1, 2, 3, 5, 8, 10, 11], [14]]
>>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
... targetdensity=0.95))
[[1, 2], [5], [8, 10, 11], [14]]
>>> list(_slicechunktodensity(revlog, [1, 2, 3, 5, 8, 10, 11, 14],
... targetdensity=0.95, mingapsize=12))
[[1, 2], [5, 8, 10, 11], [14]]
"""
start = revlog.start
length = revlog.length
if len(revs) <= 1:
yield revs
return
deltachainspan = segmentspan(revlog, revs)
if deltachainspan < mingapsize:
yield revs
return
readdata = deltachainspan
chainpayload = sum(length(r) for r in revs)
if deltachainspan:
density = chainpayload / float(deltachainspan)
else:
density = 1.0
if density >= targetdensity:
yield revs
return
# Store the gaps in a heap to have them sorted by decreasing size
gaps = []
prevend = None
for i, rev in enumerate(revs):
revstart = start(rev)
revlen = length(rev)
# Skip empty revisions to form larger holes
if revlen == 0:
continue
if prevend is not None:
gapsize = revstart - prevend
# only consider holes that are large enough
if gapsize > mingapsize:
gaps.append((gapsize, i))
prevend = revstart + revlen
# sort the gaps to pop them from largest to small
gaps.sort()
# Collect the indices of the largest holes until the density is acceptable
selected = []
while gaps and density < targetdensity:
gapsize, gapidx = gaps.pop()
selected.append(gapidx)
# the gap sizes are stored as negatives to be sorted decreasingly
# by the heap
readdata -= gapsize
if readdata > 0:
density = chainpayload / float(readdata)
else:
density = 1.0
selected.sort()
# Cut the revs at collected indices
previdx = 0
for idx in selected:
chunk = _trimchunk(revlog, revs, previdx, idx)
if chunk:
yield chunk
previdx = idx
chunk = _trimchunk(revlog, revs, previdx)
if chunk:
yield chunk
def _trimchunk(revlog, revs, startidx, endidx=None):
"""returns revs[startidx:endidx] without empty trailing revs
Doctest Setup
>>> revlog = _testrevlog([
... 5, #0
... 10, #1
... 12, #2
... 12, #3 (empty)
... 17, #4
... 21, #5
... 21, #6 (empty)
... ])
Contiguous cases:
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0)
[0, 1, 2, 3, 4, 5]
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 5)
[0, 1, 2, 3, 4]
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 0, 4)
[0, 1, 2]
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 2, 4)
[2]
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3)
[3, 4, 5]
>>> _trimchunk(revlog, [0, 1, 2, 3, 4, 5, 6], 3, 5)
[3, 4]
Discontiguous cases:
>>> _trimchunk(revlog, [1, 3, 5, 6], 0)
[1, 3, 5]
>>> _trimchunk(revlog, [1, 3, 5, 6], 0, 2)
[1]
>>> _trimchunk(revlog, [1, 3, 5, 6], 1, 3)
[3, 5]
>>> _trimchunk(revlog, [1, 3, 5, 6], 1)
[3, 5]
"""
length = revlog.length
if endidx is None:
endidx = len(revs)
# If we have a non-emtpy delta candidate, there are nothing to trim
if revs[endidx - 1] < len(revlog):
# Trim empty revs at the end, except the very first revision of a chain
while (
endidx > 1 and endidx > startidx and length(revs[endidx - 1]) == 0
):
endidx -= 1
return revs[startidx:endidx]
def segmentspan(revlog, revs):
"""Get the byte span of a segment of revisions
revs is a sorted array of revision numbers
>>> revlog = _testrevlog([
... 5, #0
... 10, #1
... 12, #2
... 12, #3 (empty)
... 17, #4
... ])
>>> segmentspan(revlog, [0, 1, 2, 3, 4])
17
>>> segmentspan(revlog, [0, 4])
17
>>> segmentspan(revlog, [3, 4])
5
>>> segmentspan(revlog, [1, 2, 3,])
7
>>> segmentspan(revlog, [1, 3])
7
"""
if not revs:
return 0
end = revlog.end(revs[-1])
return end - revlog.start(revs[0])
def _textfromdelta(fh, revlog, baserev, delta, p1, p2, flags, expectednode):
"""build full text from a (base, delta) pair and other metadata"""
# special case deltas which replace entire base; no need to decode
# base revision. this neatly avoids censored bases, which throw when
# they're decoded.
hlen = struct.calcsize(b">lll")
if delta[:hlen] == mdiff.replacediffheader(
revlog.rawsize(baserev), len(delta) - hlen
):
fulltext = delta[hlen:]
else:
# deltabase is rawtext before changed by flag processors, which is
# equivalent to non-raw text
basetext = revlog.revision(baserev, _df=fh, raw=False)
fulltext = mdiff.patch(basetext, delta)
try:
validatehash = flagutil.processflagsraw(revlog, fulltext, flags)
if validatehash:
revlog.checkhash(fulltext, expectednode, p1=p1, p2=p2)
if flags & REVIDX_ISCENSORED:
raise error.StorageError(
_(b'node %s is not censored') % expectednode
)
except error.CensoredNodeError:
# must pass the censored index flag to add censored revisions
if not flags & REVIDX_ISCENSORED:
raise
return fulltext
@attr.s(slots=True, frozen=True)
class _deltainfo(object):
distance = attr.ib()
deltalen = attr.ib()
data = attr.ib()
base = attr.ib()
chainbase = attr.ib()
chainlen = attr.ib()
compresseddeltalen = attr.ib()
snapshotdepth = attr.ib()
def isgooddeltainfo(revlog, deltainfo, revinfo):
"""Returns True if the given delta is good. Good means that it is within
the disk span, disk size, and chain length bounds that we know to be
performant."""
if deltainfo is None:
return False
# - 'deltainfo.distance' is the distance from the base revision --
# bounding it limits the amount of I/O we need to do.
# - 'deltainfo.compresseddeltalen' is the sum of the total size of
# deltas we need to apply -- bounding it limits the amount of CPU
# we consume.
textlen = revinfo.textlen
defaultmax = textlen * 4
maxdist = revlog._maxdeltachainspan
if not maxdist:
maxdist = deltainfo.distance # ensure the conditional pass
maxdist = max(maxdist, defaultmax)
# Bad delta from read span:
#
# If the span of data read is larger than the maximum allowed.
#
# In the sparse-revlog case, we rely on the associated "sparse reading"
# to avoid issue related to the span of data. In theory, it would be
# possible to build pathological revlog where delta pattern would lead
# to too many reads. However, they do not happen in practice at all. So
# we skip the span check entirely.
if not revlog._sparserevlog and maxdist < deltainfo.distance:
return False
# Bad delta from new delta size:
#
# If the delta size is larger than the target text, storing the
# delta will be inefficient.
if textlen < deltainfo.deltalen:
return False
# Bad delta from cumulated payload size:
#
# If the sum of delta get larger than K * target text length.
if textlen * LIMIT_DELTA2TEXT < deltainfo.compresseddeltalen:
return False
# Bad delta from chain length:
#
# If the number of delta in the chain gets too high.
if revlog._maxchainlen and revlog._maxchainlen < deltainfo.chainlen:
return False
# bad delta from intermediate snapshot size limit
#
# If an intermediate snapshot size is higher than the limit. The
# limit exist to prevent endless chain of intermediate delta to be
# created.
if (
deltainfo.snapshotdepth is not None
and (textlen >> deltainfo.snapshotdepth) < deltainfo.deltalen
):
return False
# bad delta if new intermediate snapshot is larger than the previous
# snapshot
if (
deltainfo.snapshotdepth
and revlog.length(deltainfo.base) < deltainfo.deltalen
):
return False
return True
# If a revision's full text is that much bigger than a base candidate full
# text's, it is very unlikely that it will produce a valid delta. We no longer
# consider these candidates.
LIMIT_BASE2TEXT = 500
def _candidategroups(revlog, textlen, p1, p2, cachedelta):
"""Provides group of revision to be tested as delta base
This top level function focus on emitting groups with unique and worthwhile
content. See _raw_candidate_groups for details about the group order.
"""
# should we try to build a delta?
if not (len(revlog) and revlog._storedeltachains):
yield None
return
deltalength = revlog.length
deltaparent = revlog.deltaparent
sparse = revlog._sparserevlog
good = None
deltas_limit = textlen * LIMIT_DELTA2TEXT
tested = {nullrev}
candidates = _refinedgroups(revlog, p1, p2, cachedelta)
while True:
temptative = candidates.send(good)
if temptative is None:
break
group = []
for rev in temptative:
# skip over empty delta (no need to include them in a chain)
while revlog._generaldelta and not (
rev == nullrev or rev in tested or deltalength(rev)
):
tested.add(rev)
rev = deltaparent(rev)
# no need to try a delta against nullrev, this will be done as a
# last resort.
if rev == nullrev:
continue
# filter out revision we tested already
if rev in tested:
continue
tested.add(rev)
# filter out delta base that will never produce good delta
if deltas_limit < revlog.length(rev):
continue
if sparse and revlog.rawsize(rev) < (textlen // LIMIT_BASE2TEXT):
continue
# no delta for rawtext-changing revs (see "candelta" for why)
if revlog.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS:
continue
# If we reach here, we are about to build and test a delta.
# The delta building process will compute the chaininfo in all
# case, since that computation is cached, it is fine to access it
# here too.
chainlen, chainsize = revlog._chaininfo(rev)
# if chain will be too long, skip base
if revlog._maxchainlen and chainlen >= revlog._maxchainlen:
continue
# if chain already have too much data, skip base
if deltas_limit < chainsize:
continue
if sparse and revlog.upperboundcomp is not None:
maxcomp = revlog.upperboundcomp
basenotsnap = (p1, p2, nullrev)
if rev not in basenotsnap and revlog.issnapshot(rev):
snapshotdepth = revlog.snapshotdepth(rev)
# If text is significantly larger than the base, we can
# expect the resulting delta to be proportional to the size
# difference
revsize = revlog.rawsize(rev)
rawsizedistance = max(textlen - revsize, 0)
# use an estimate of the compression upper bound.
lowestrealisticdeltalen = rawsizedistance // maxcomp
# check the absolute constraint on the delta size
snapshotlimit = textlen >> snapshotdepth
if snapshotlimit < lowestrealisticdeltalen:
# delta lower bound is larger than accepted upper bound
continue
# check the relative constraint on the delta size
revlength = revlog.length(rev)
if revlength < lowestrealisticdeltalen:
# delta probable lower bound is larger than target base
continue
group.append(rev)
if group:
# XXX: in the sparse revlog case, group can become large,
# impacting performances. Some bounding or slicing mecanism
# would help to reduce this impact.
good = yield tuple(group)
yield None
def _findsnapshots(revlog, cache, start_rev):
"""find snapshot from start_rev to tip"""
if util.safehasattr(revlog.index, b'findsnapshots'):
revlog.index.findsnapshots(cache, start_rev)
else:
deltaparent = revlog.deltaparent
issnapshot = revlog.issnapshot
for rev in revlog.revs(start_rev):
if issnapshot(rev):
cache[deltaparent(rev)].append(rev)
def _refinedgroups(revlog, p1, p2, cachedelta):
good = None
# First we try to reuse a the delta contained in the bundle.
# (or from the source revlog)
#
# This logic only applies to general delta repositories and can be disabled
# through configuration. Disabling reuse source delta is useful when
# we want to make sure we recomputed "optimal" deltas.
if cachedelta and revlog._generaldelta and revlog._lazydeltabase:
# Assume what we received from the server is a good choice
# build delta will reuse the cache
good = yield (cachedelta[0],)
if good is not None:
yield None
return
snapshots = collections.defaultdict(list)
for candidates in _rawgroups(revlog, p1, p2, cachedelta, snapshots):
good = yield candidates
if good is not None:
break
# If sparse revlog is enabled, we can try to refine the available deltas
if not revlog._sparserevlog:
yield None
return
# if we have a refinable value, try to refine it
if good is not None and good not in (p1, p2) and revlog.issnapshot(good):
# refine snapshot down
previous = None
while previous != good:
previous = good
base = revlog.deltaparent(good)
if base == nullrev:
break
good = yield (base,)
# refine snapshot up
if not snapshots:
_findsnapshots(revlog, snapshots, good + 1)
previous = None
while good != previous:
previous = good
children = tuple(sorted(c for c in snapshots[good]))
good = yield children
# we have found nothing
yield None
def _rawgroups(revlog, p1, p2, cachedelta, snapshots=None):
"""Provides group of revision to be tested as delta base
This lower level function focus on emitting delta theorically interresting
without looking it any practical details.
The group order aims at providing fast or small candidates first.
"""
gdelta = revlog._generaldelta
# gate sparse behind general-delta because of issue6056
sparse = gdelta and revlog._sparserevlog
curr = len(revlog)
prev = curr - 1
deltachain = lambda rev: revlog._deltachain(rev)[0]
if gdelta:
# exclude already lazy tested base if any
parents = [p for p in (p1, p2) if p != nullrev]
if not revlog._deltabothparents and len(parents) == 2:
parents.sort()
# To minimize the chance of having to build a fulltext,
# pick first whichever parent is closest to us (max rev)
yield (parents[1],)
# then the other one (min rev) if the first did not fit
yield (parents[0],)
elif len(parents) > 0:
# Test all parents (1 or 2), and keep the best candidate
yield parents
if sparse and parents:
if snapshots is None:
# map: base-rev: snapshot-rev
snapshots = collections.defaultdict(list)
# See if we can use an existing snapshot in the parent chains to use as
# a base for a new intermediate-snapshot
#
# search for snapshot in parents delta chain
# map: snapshot-level: snapshot-rev
parents_snaps = collections.defaultdict(set)
candidate_chains = [deltachain(p) for p in parents]
for chain in candidate_chains:
for idx, s in enumerate(chain):
if not revlog.issnapshot(s):
break
parents_snaps[idx].add(s)
snapfloor = min(parents_snaps[0]) + 1
_findsnapshots(revlog, snapshots, snapfloor)
# search for the highest "unrelated" revision
#
# Adding snapshots used by "unrelated" revision increase the odd we
# reuse an independant, yet better snapshot chain.
#
# XXX instead of building a set of revisions, we could lazily enumerate
# over the chains. That would be more efficient, however we stick to
# simple code for now.
all_revs = set()
for chain in candidate_chains:
all_revs.update(chain)
other = None
for r in revlog.revs(prev, snapfloor):
if r not in all_revs:
other = r
break
if other is not None:
# To avoid unfair competition, we won't use unrelated intermediate
# snapshot that are deeper than the ones from the parent delta
# chain.
max_depth = max(parents_snaps.keys())
chain = deltachain(other)
for idx, s in enumerate(chain):
if s < snapfloor:
continue
if max_depth < idx:
break
if not revlog.issnapshot(s):
break
parents_snaps[idx].add(s)
# Test them as possible intermediate snapshot base
# We test them from highest to lowest level. High level one are more
# likely to result in small delta
floor = None
for idx, snaps in sorted(parents_snaps.items(), reverse=True):
siblings = set()
for s in snaps:
siblings.update(snapshots[s])
# Before considering making a new intermediate snapshot, we check
# if an existing snapshot, children of base we consider, would be
# suitable.
#
# It give a change to reuse a delta chain "unrelated" to the
# current revision instead of starting our own. Without such
# re-use, topological branches would keep reopening new chains.
# Creating more and more snapshot as the repository grow.
if floor is not None:
# We only do this for siblings created after the one in our
# parent's delta chain. Those created before has less chances
# to be valid base since our ancestors had to create a new
# snapshot.
siblings = [r for r in siblings if floor < r]
yield tuple(sorted(siblings))
# then test the base from our parent's delta chain.
yield tuple(sorted(snaps))
floor = min(snaps)
# No suitable base found in the parent chain, search if any full
# snapshots emitted since parent's base would be a suitable base for an
# intermediate snapshot.
#
# It give a chance to reuse a delta chain unrelated to the current
# revisions instead of starting our own. Without such re-use,
# topological branches would keep reopening new full chains. Creating
# more and more snapshot as the repository grow.
yield tuple(snapshots[nullrev])
if not sparse:
# other approach failed try against prev to hopefully save us a
# fulltext.
yield (prev,)
class deltacomputer(object):
def __init__(self, revlog):
self.revlog = revlog
def buildtext(self, revinfo, fh):
"""Builds a fulltext version of a revision
revinfo: _revisioninfo instance that contains all needed info
fh: file handle to either the .i or the .d revlog file,
depending on whether it is inlined or not
"""
btext = revinfo.btext
if btext[0] is not None:
return btext[0]
revlog = self.revlog
cachedelta = revinfo.cachedelta
baserev = cachedelta[0]
delta = cachedelta[1]
fulltext = btext[0] = _textfromdelta(
fh,
revlog,
baserev,
delta,
revinfo.p1,
revinfo.p2,
revinfo.flags,
revinfo.node,
)
return fulltext
def _builddeltadiff(self, base, revinfo, fh):
revlog = self.revlog
t = self.buildtext(revinfo, fh)
if revlog.iscensored(base):
# deltas based on a censored revision must replace the
# full content in one patch, so delta works everywhere
header = mdiff.replacediffheader(revlog.rawsize(base), len(t))
delta = header + t
else:
ptext = revlog.rawdata(base, _df=fh)
delta = mdiff.textdiff(ptext, t)
return delta
def _builddeltainfo(self, revinfo, base, fh):
# can we use the cached delta?
revlog = self.revlog
chainbase = revlog.chainbase(base)
if revlog._generaldelta:
deltabase = base
else:
deltabase = chainbase
snapshotdepth = None
if revlog._sparserevlog and deltabase == nullrev:
snapshotdepth = 0
elif revlog._sparserevlog and revlog.issnapshot(deltabase):
# A delta chain should always be one full snapshot,
# zero or more semi-snapshots, and zero or more deltas
p1, p2 = revlog.rev(revinfo.p1), revlog.rev(revinfo.p2)
if deltabase not in (p1, p2) and revlog.issnapshot(deltabase):
snapshotdepth = len(revlog._deltachain(deltabase)[0])
delta = None
if revinfo.cachedelta:
cachebase, cachediff = revinfo.cachedelta
# check if the diff still apply
currentbase = cachebase
while (
currentbase != nullrev
and currentbase != base
and self.revlog.length(currentbase) == 0
):
currentbase = self.revlog.deltaparent(currentbase)
if self.revlog._lazydelta and currentbase == base:
delta = revinfo.cachedelta[1]
if delta is None:
delta = self._builddeltadiff(base, revinfo, fh)
# snapshotdept need to be neither None nor 0 level snapshot
if revlog.upperboundcomp is not None and snapshotdepth:
lowestrealisticdeltalen = len(delta) // revlog.upperboundcomp
snapshotlimit = revinfo.textlen >> snapshotdepth
if snapshotlimit < lowestrealisticdeltalen:
return None
if revlog.length(base) < lowestrealisticdeltalen:
return None
header, data = revlog.compress(delta)
deltalen = len(header) + len(data)
offset = revlog.end(len(revlog) - 1)
dist = deltalen + offset - revlog.start(chainbase)
chainlen, compresseddeltalen = revlog._chaininfo(base)
chainlen += 1
compresseddeltalen += deltalen
return _deltainfo(
dist,
deltalen,
(header, data),
deltabase,
chainbase,
chainlen,
compresseddeltalen,
snapshotdepth,
)
def _fullsnapshotinfo(self, fh, revinfo):
curr = len(self.revlog)
rawtext = self.buildtext(revinfo, fh)
data = self.revlog.compress(rawtext)
compresseddeltalen = deltalen = dist = len(data[1]) + len(data[0])
deltabase = chainbase = curr
snapshotdepth = 0
chainlen = 1
return _deltainfo(
dist,
deltalen,
data,
deltabase,
chainbase,
chainlen,
compresseddeltalen,
snapshotdepth,
)
def finddeltainfo(self, revinfo, fh):
"""Find an acceptable delta against a candidate revision
revinfo: information about the revision (instance of _revisioninfo)
fh: file handle to either the .i or the .d revlog file,
depending on whether it is inlined or not
Returns the first acceptable candidate revision, as ordered by
_candidategroups
If no suitable deltabase is found, we return delta info for a full
snapshot.
"""
if not revinfo.textlen:
return self._fullsnapshotinfo(fh, revinfo)
# no delta for flag processor revision (see "candelta" for why)
# not calling candelta since only one revision needs test, also to
# avoid overhead fetching flags again.
if revinfo.flags & REVIDX_RAWTEXT_CHANGING_FLAGS:
return self._fullsnapshotinfo(fh, revinfo)
cachedelta = revinfo.cachedelta
p1 = revinfo.p1
p2 = revinfo.p2
revlog = self.revlog
deltainfo = None
p1r, p2r = revlog.rev(p1), revlog.rev(p2)
groups = _candidategroups(
self.revlog, revinfo.textlen, p1r, p2r, cachedelta
)
candidaterevs = next(groups)
while candidaterevs is not None:
nominateddeltas = []
if deltainfo is not None:
# if we already found a good delta,
# challenge it against refined candidates
nominateddeltas.append(deltainfo)
for candidaterev in candidaterevs:
candidatedelta = self._builddeltainfo(revinfo, candidaterev, fh)
if candidatedelta is not None:
if isgooddeltainfo(self.revlog, candidatedelta, revinfo):
nominateddeltas.append(candidatedelta)
if nominateddeltas:
deltainfo = min(nominateddeltas, key=lambda x: x.deltalen)
if deltainfo is not None:
candidaterevs = groups.send(deltainfo.base)
else:
candidaterevs = next(groups)
if deltainfo is None:
deltainfo = self._fullsnapshotinfo(fh, revinfo)
return deltainfo