bdiff: extend matches across popular lines
For very large diffs that have large numbers of identical lines (JSON
dumps) that also have large blocks of identical text, bdiff could become
confused about which block matches which because it can only match
very limited regions. The result is very large diffs for small sets of edits.
The earlier recursion rebalancing fix made this behavior more frequent because
it's now more prone to match block 1 to block 2. One frequent user of
large JSON files reported being unable to pass the resulting diffs
through their code review system.
Prior to this change, bdiff would calculate the length of a match at
(i, j) as 1 + length found at (i-1, j-1). With large number of popular
(ignored) lines, this often meant matches couldn't be extended
backwards at all and thus all matching regions were very small.
Disabling the popularity threshold is not an option because it brings
back quadratic behavior.
Instead, we extend a match backwards until we either found a previously
discovered match or we find a mismatching line. This thus successfully
bridges over any popular lines inside and before a matching region.
The larger regions then significant reduce the probability of confusion.
--- a/mercurial/bdiff.c Thu Jun 02 16:18:44 2016 -0700
+++ b/mercurial/bdiff.c Thu Jun 02 17:09:06 2016 -0500
@@ -166,10 +166,17 @@
/* loop through all lines match a[i] in b */
for (; j >= b1; j = b[j].n) {
/* does this extend an earlier match? */
- if (i > a1 && j > b1 && pos[j - 1].pos == i - 1)
- k = pos[j - 1].len + 1;
- else
- k = 1;
+ for (k = 1; j - k >= b1 && i - k >= a1; k++) {
+ /* reached an earlier match? */
+ if (pos[j - k].pos == i - k) {
+ k += pos[j - k].len;
+ break;
+ }
+ /* previous line mismatch? */
+ if (a[i - k].e != b[j - k].e)
+ break;
+ }
+
pos[j].pos = i;
pos[j].len = k;