xdiff: add a preprocessing step that trims files
xdiff has a `xdl_trim_ends` step that removes common lines, unmatchable
lines. That is in theory good, but happens too late - after splitting,
hashing, and adjusting the hash values so they are unique. Those splitting,
hashing and adjusting hash values steps could have noticeable overhead.
Diffing two large files with minor (one-line-ish) changes are not uncommon.
In that case, the raw performance of those preparation steps seriously
matter. Even allocating an O(N) array and storing line offsets to it is
expensive. Therefore my previous attempts [1] [2] cannot be good enough
since they do not remove the O(N) array assignment.
This patch adds a preprocessing step - `xdl_trim_files` that runs before
other preprocessing steps. It counts common prefix and suffix and lines in
them (needed for displaying line number), without doing anything else.
Testing with a crafted large (169MB) file, with minor change:
```
open('a','w').write(''.join('%s\n' % (i % 100000) for i in xrange(
30000000) if i != 6000000))
open('b','w').write(''.join('%s\n' % (i % 100000) for i in xrange(
30000000) if i != 6003000))
```
Running xdiff by a simple binary [3], this patch improves the xdiff perf by
more than 10x for the above case:
```
# xdiff before this patch
2.41s user 1.13s system 98% cpu 3.592 total
# xdiff after this patch
0.14s user 0.16s system 98% cpu 0.309 total
# gnu diffutils
0.12s user 0.15s system 98% cpu 0.272 total
# (best of 20 runs)
```
It's still slightly slower than GNU diffutils. But it's pretty close now.
Testing with real repo data:
For the whole repo, this patch makes xdiff 25% faster:
```
# hg perfbdiff --count 100 --alldata -c
d334afc585e2 --blocks [--xdiff]
# xdiff, after
! wall 0.058861 comb 0.050000 user 0.050000 sys 0.000000 (best of 100)
# xdiff, before
! wall 0.077816 comb 0.080000 user 0.080000 sys 0.000000 (best of 91)
# bdiff
! wall 0.117473 comb 0.120000 user 0.120000 sys 0.000000 (best of 67)
```
For files that are long (ex. commands.py), the speedup is more than 3x, very
significant:
```
# hg perfbdiff --count 3000 --blocks commands.py.i 1 [--xdiff]
# xdiff, after
! wall 0.690583 comb 0.690000 user 0.690000 sys 0.000000 (best of 12)
# xdiff, before
! wall 2.240361 comb 2.210000 user 2.210000 sys 0.000000 (best of 4)
# bdiff
! wall 2.469852 comb 2.440000 user 2.440000 sys 0.000000 (best of 4)
```
[1]: https://phab.mercurial-scm.org/D2631
[2]: https://phab.mercurial-scm.org/D2634
[3]:
```
// Code to run xdiff from command line. No proper error handling.
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "mercurial/thirdparty/xdiff/xdiff.h"
#define ensure(x) if (!(x)) exit(255);
mmfile_t readfile(const char *path) {
struct stat st; int fd = open(path, O_RDONLY);
fstat(fd, &st); mmfile_t file = { malloc(st.st_size), st.st_size };
ensure(read(fd, file.ptr, st.st_size) == st.st_size); close(fd);
return file;
}
int main(int argc, char const *argv[]) {
mmfile_t a = readfile(argv[1]), b = readfile(argv[2]);
xpparam_t xpp = {0}; xdemitconf_t xecfg = {0}; xdemitcb_t ecb = {0};
xdl_diff(&a, &b, &xpp, &xecfg, &ecb);
return 0;
}
```
Differential Revision: https://phab.mercurial-scm.org/D2686
# Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
#
# This is a small extension for Mercurial (https://mercurial-scm.org/)
# that removes files not known to mercurial
#
# This program was inspired by the "cvspurge" script contained in CVS
# utilities (http://www.red-bean.com/cvsutils/).
#
# For help on the usage of "hg purge" use:
# hg help purge
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
'''command to delete untracked files from the working directory'''
from __future__ import absolute_import
import os
from mercurial.i18n import _
from mercurial import (
cmdutil,
error,
pycompat,
registrar,
scmutil,
util,
)
cmdtable = {}
command = registrar.command(cmdtable)
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
# be specifying the version(s) of Mercurial they are tested with, or
# leave the attribute unspecified.
testedwith = 'ships-with-hg-core'
@command('purge|clean',
[('a', 'abort-on-err', None, _('abort if an error occurs')),
('', 'all', None, _('purge ignored files too')),
('', 'dirs', None, _('purge empty directories')),
('', 'files', None, _('purge files')),
('p', 'print', None, _('print filenames instead of deleting them')),
('0', 'print0', None, _('end filenames with NUL, for use with xargs'
' (implies -p/--print)')),
] + cmdutil.walkopts,
_('hg purge [OPTION]... [DIR]...'))
def purge(ui, repo, *dirs, **opts):
'''removes files not tracked by Mercurial
Delete files not known to Mercurial. This is useful to test local
and uncommitted changes in an otherwise-clean source tree.
This means that purge will delete the following by default:
- Unknown files: files marked with "?" by :hg:`status`
- Empty directories: in fact Mercurial ignores directories unless
they contain files under source control management
But it will leave untouched:
- Modified and unmodified tracked files
- Ignored files (unless --all is specified)
- New files added to the repository (with :hg:`add`)
The --files and --dirs options can be used to direct purge to delete
only files, only directories, or both. If neither option is given,
both will be deleted.
If directories are given on the command line, only files in these
directories are considered.
Be careful with purge, as you could irreversibly delete some files
you forgot to add to the repository. If you only want to print the
list of files that this program would delete, use the --print
option.
'''
opts = pycompat.byteskwargs(opts)
act = not opts.get('print')
eol = '\n'
if opts.get('print0'):
eol = '\0'
act = False # --print0 implies --print
removefiles = opts.get('files')
removedirs = opts.get('dirs')
if not removefiles and not removedirs:
removefiles = True
removedirs = True
def remove(remove_func, name):
if act:
try:
remove_func(repo.wjoin(name))
except OSError:
m = _('%s cannot be removed') % name
if opts.get('abort_on_err'):
raise error.Abort(m)
ui.warn(_('warning: %s\n') % m)
else:
ui.write('%s%s' % (name, eol))
match = scmutil.match(repo[None], dirs, opts)
if removedirs:
directories = []
match.explicitdir = match.traversedir = directories.append
status = repo.status(match=match, ignored=opts.get('all'), unknown=True)
if removefiles:
for f in sorted(status.unknown + status.ignored):
if act:
ui.note(_('removing file %s\n') % f)
remove(util.unlink, f)
if removedirs:
for f in sorted(directories, reverse=True):
if match(f) and not os.listdir(repo.wjoin(f)):
if act:
ui.note(_('removing directory %s\n') % f)
remove(os.rmdir, f)