Mercurial > hg
view tests/test-linelog.py @ 42222:57203e0210f8
copies: calculate mergecopies() based on pathcopies()
When copies are stored in changesets, we need a changeset-centric
version of mergecopies() just like we have a changeset-centric version
of pathcopies(). I think the natural way of thinking about
mergecopies() is in terms of pathcopies() from the base to each of the
commits. So if we can rewrite mergecopies() based on two such
pathcopies() calls, we'll get the changeset-centric version for
free. That's what this patch does.
A nice bonus is that it ends up being a lot simpler. mergecopies() has
accumulated a lot of technical debt over time. One good example is the
code for dealing with grafts (the "partial/incomplete/dirty"
stuff). Since pathcopies() already deals with backwards renames and
ping-pong renames, we get that for free.
I've run tests with hard-coded debug logging for "fullcopy" and while
I haven't looked at every difference it produces, all the ones I have
looked at seemed reasonable to me. I'm a little surprised that no more
tests fail when run with '--extra-config-opt
experimental.copies.read-from=compatibility' compared to before this
patch. This patch also fixes the broken cases in test-annotate.t and
test-fastannotate.t. It also enables the part of test-copies.t that
was previously disabled exactly because mergecopies() needed to get a
changeset-centric version.
One drawback of the rewritten code is that we may now make
remotefilelog prefetch more files. We used to prefetch files that were
unique to either side of the merge compared to the other. We now
prefetch files that are unique to either side of the merge compared to
the base. This means that if you added the same file to each side, we
would not prefetch it before, but we would now. Such cases are
probably quite rare, but one likely scenario where they happen is when
moving from a commit to its successor (or the other way around). The
user will probably already have the files in the cache in such cases,
so it's probably not a big deal.
Some timings for calculating mergecopies between two revisions
(revisions shown on each line, all using the common ancestor as base):
In the hg repo:
4.8 4.9: 0.21s -> 0.21s
4.0 4.8: 0.35s -> 0.63s
In and old copy of the mozilla-unified repo:
FIREFOX_BETA_60_BASE^ FIREFOX_BETA_60_BASE: 0.82s -> 0.82s
FIREFOX_NIGHTLY_59_END FIREFOX_BETA_60_BASE: 2.5s -> 2.6s
FIREFOX_BETA_59_END FIREFOX_BETA_60_BASE: 3.9s -> 4.1s
FIREFOX_AURORA_50_BASE FIREFOX_BETA_60_BASE: 31s -> 33s
So it's measurably slower in most cases. The most significant
difference is in the hg repo between revisions 4.0 and 4.8. In that
case it seems to come from the fact that pathcopies() uses
fctx.isintroducedafter() (in _tracefile), while the old mergecopies()
used fctx.linkrev() (in _checkcopies()). That results in a single call
to filectx._adjustlinkrev(), which is responsible for the entire
difference in time (in my repo). So we pay a performance penalty but
we get more correct code (see change in
test-mv-cp-st-diff.t). Deleting the "== f.filenode()" in _tracefile()
recovers the lost performance in the hg repo.
There were are few other optimizations in _checkcopies() that I could
not measure any impact from. One was from the "seen" set. Another was
from a "continue" when the file was not in the destination manifest
(corresponding to "am" in _tracefile).
Also note that merge copies are not calculated when updating with a
clean working copy, which is probably the most common case. I
therefore think the much simpler code is worth the slowdown.
Differential Revision: https://phab.mercurial-scm.org/D6255
author | Martin von Zweigbergk <martinvonz@google.com> |
---|---|
date | Thu, 11 Apr 2019 23:22:54 -0700 |
parents | 876494fd967d |
children | 2372284d9457 |
line wrap: on
line source
from __future__ import absolute_import, print_function import difflib import random import unittest from mercurial import linelog vecratio = 3 # number of replacelines / number of replacelines_vec maxlinenum = 0xffffff maxb1 = 0xffffff maxdeltaa = 10 maxdeltab = 10 def _genedits(seed, endrev): lines = [] random.seed(seed) for rev in range(0, endrev): n = len(lines) a1 = random.randint(0, n) a2 = random.randint(a1, min(n, a1 + maxdeltaa)) b1 = random.randint(0, maxb1) b2 = random.randint(b1, b1 + maxdeltab) usevec = not bool(random.randint(0, vecratio)) if usevec: blines = [(random.randint(0, rev), random.randint(0, maxlinenum)) for _ in range(b1, b2)] else: blines = [(rev, bidx) for bidx in range(b1, b2)] lines[a1:a2] = blines yield lines, rev, a1, a2, b1, b2, blines, usevec class linelogtests(unittest.TestCase): def testlinelogencodedecode(self): program = [linelog._eof(0, 0), linelog._jge(41, 42), linelog._jump(0, 43), linelog._eof(0, 0), linelog._jl(44, 45), linelog._line(46, 47), ] ll = linelog.linelog(program, maxrev=100) enc = ll.encode() # round-trips okay self.assertEqual(linelog.linelog.fromdata(enc)._program, ll._program) self.assertEqual(linelog.linelog.fromdata(enc), ll) # This encoding matches the encoding used by hg-experimental's # linelog file, or is supposed to if it doesn't. self.assertEqual(enc, (b'\x00\x00\x01\x90\x00\x00\x00\x06' b'\x00\x00\x00\xa4\x00\x00\x00*' b'\x00\x00\x00\x00\x00\x00\x00+' b'\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\xb1\x00\x00\x00-' b'\x00\x00\x00\xba\x00\x00\x00/')) def testsimpleedits(self): ll = linelog.linelog() # Initial revision: add lines 0, 1, and 2 ll.replacelines(1, 0, 0, 0, 3) self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(1)], [(1, 0), (1, 1), (1, 2), ]) # Replace line 1 with a new line ll.replacelines(2, 1, 2, 1, 2) self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(2)], [(1, 0), (2, 1), (1, 2), ]) # delete a line out of 2 ll.replacelines(3, 1, 2, 0, 0) self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(3)], [(1, 0), (1, 2), ]) # annotation of 1 is unchanged self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(1)], [(1, 0), (1, 1), (1, 2), ]) ll.annotate(3) # set internal state to revision 3 start = ll.getoffset(0) end = ll.getoffset(1) self.assertEqual(ll.getalllines(start, end), [ (1, 0), (2, 1), (1, 1), ]) self.assertEqual(ll.getalllines(), [ (1, 0), (2, 1), (1, 1), (1, 2), ]) def testparseclinelogfile(self): # This data is what the replacements in testsimpleedits # produce when fed to the original linelog.c implementation. data = (b'\x00\x00\x00\x0c\x00\x00\x00\x0f' b'\x00\x00\x00\x00\x00\x00\x00\x02' b'\x00\x00\x00\x05\x00\x00\x00\x06' b'\x00\x00\x00\x06\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x07' b'\x00\x00\x00\x06\x00\x00\x00\x02' b'\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\t\x00\x00\x00\t' b'\x00\x00\x00\x00\x00\x00\x00\x0c' b'\x00\x00\x00\x08\x00\x00\x00\x05' b'\x00\x00\x00\x06\x00\x00\x00\x01' b'\x00\x00\x00\x00\x00\x00\x00\x05' b'\x00\x00\x00\x0c\x00\x00\x00\x05' b'\x00\x00\x00\n\x00\x00\x00\x01' b'\x00\x00\x00\x00\x00\x00\x00\t') llc = linelog.linelog.fromdata(data) self.assertEqual([(l.rev, l.linenum) for l in llc.annotate(1)], [(1, 0), (1, 1), (1, 2), ]) self.assertEqual([(l.rev, l.linenum) for l in llc.annotate(2)], [(1, 0), (2, 1), (1, 2), ]) self.assertEqual([(l.rev, l.linenum) for l in llc.annotate(3)], [(1, 0), (1, 2), ]) # Check we emit the same bytecode. ll = linelog.linelog() # Initial revision: add lines 0, 1, and 2 ll.replacelines(1, 0, 0, 0, 3) # Replace line 1 with a new line ll.replacelines(2, 1, 2, 1, 2) # delete a line out of 2 ll.replacelines(3, 1, 2, 0, 0) diff = '\n ' + '\n '.join(difflib.unified_diff( ll.debugstr().splitlines(), llc.debugstr().splitlines(), 'python', 'c', lineterm='')) self.assertEqual(ll._program, llc._program, 'Program mismatch: ' + diff) # Done as a secondary step so we get a better result if the # program is where the mismatch is. self.assertEqual(ll, llc) self.assertEqual(ll.encode(), data) def testanothersimplecase(self): ll = linelog.linelog() ll.replacelines(3, 0, 0, 0, 2) ll.replacelines(4, 0, 2, 0, 0) self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(4)], []) self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(3)], [(3, 0), (3, 1)]) # rev 2 is empty because contents were only ever introduced in rev 3 self.assertEqual([(l.rev, l.linenum) for l in ll.annotate(2)], []) def testrandomedits(self): # Inspired by original linelog tests. seed = random.random() numrevs = 2000 ll = linelog.linelog() # Populate linelog for lines, rev, a1, a2, b1, b2, blines, usevec in _genedits( seed, numrevs): if usevec: ll.replacelines_vec(rev, a1, a2, blines) else: ll.replacelines(rev, a1, a2, b1, b2) ar = ll.annotate(rev) self.assertEqual(ll.annotateresult, lines) # Verify we can get back these states by annotating each rev for lines, rev, a1, a2, b1, b2, blines, usevec in _genedits( seed, numrevs): ar = ll.annotate(rev) self.assertEqual([(l.rev, l.linenum) for l in ar], lines) def testinfinitebadprogram(self): ll = linelog.linelog.fromdata( b'\x00\x00\x00\x00\x00\x00\x00\x02' # header b'\x00\x00\x00\x00\x00\x00\x00\x01' # JUMP to self ) with self.assertRaises(linelog.LineLogError): # should not be an infinite loop and raise ll.annotate(1) if __name__ == '__main__': import silenttestrunner silenttestrunner.main(__name__)