fncache: avoid loading the filename cache when not actually modifying it
With time, fncache can become very large. The mozilla-central repo for example,
has a 31M and growing fncache file. Loading this file takes time (280ms for the
mozilla-central repository).
In many scenarios, we don't need to load fncache at all. For example, when
committing changes to existing files, or pushing such commits to another clone.
This patch detects when a name is added via store.vfs(), and only loads the
cache if a) the data metadata file doesn't already exist, or b) when opening
for appending, the data or metadata file exists but has size (a transaction
rollback leaves behind such files).
Benchmarks (run on Macos 10.13 on a 2017-model Macbook Pro with Core i7 2.9GHz
and flash drive), each test without and with patch run 5 times:
* committing to an existing file, against the mozilla-central repository.
Baseline real time average 2.3736, with patch 1.9884.
* unbundling a large changeset consisting *only* of existing-file modifications
(159 revisions, 1050 modifications, mozilla-central
4a250a0e4f29:
beea9ac7d823), into a clone limited to the ancestor revision of
that revset). Baseline real time average 1.5048, with patch 1.3108.
--- a/mercurial/store.py Wed Jul 11 16:11:33 2018 +0200
+++ b/mercurial/store.py Wed Jul 11 14:28:13 2018 +0100
@@ -489,10 +489,20 @@
self.encode = encode
def __call__(self, path, mode='r', *args, **kw):
+ encoded = self.encode(path)
if mode not in ('r', 'rb') and (path.startswith('data/') or
path.startswith('meta/')):
- self.fncache.add(path)
- return self.vfs(self.encode(path), mode, *args, **kw)
+ # do not trigger a fncache load when adding a file that already is
+ # known to exist.
+ notload = self.fncache.entries is None and self.vfs.exists(encoded)
+ if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
+ # when appending to an existing file, if the file has size zero,
+ # it should be considered as missing. Such zero-size files are
+ # the result of truncation when a transaction is aborted.
+ notload = False
+ if not notload:
+ self.fncache.add(path)
+ return self.vfs(encoded, mode, *args, **kw)
def join(self, path):
if path:
--- a/tests/test-fncache.t Wed Jul 11 16:11:33 2018 +0200
+++ b/tests/test-fncache.t Wed Jul 11 14:28:13 2018 +0100
@@ -436,3 +436,73 @@
$ cat .hg/store/fncache | sort
data/.bar.i
data/foo.i
+
+ $ cd ..
+
+In repositories that have accumulated a large number of files over time, the
+fncache file is going to be large. If we possibly can avoid loading it, so much the better.
+The cache should not loaded when committing changes to existing files, or when unbundling
+changesets that only contain changes to existing files:
+
+ $ cat > fncacheloadwarn.py << EOF
+ > from __future__ import absolute_import
+ > from mercurial import extensions, store
+ >
+ > def extsetup(ui):
+ > def wrapstore(orig, requirements, *args):
+ > store = orig(requirements, *args)
+ > if 'store' in requirements and 'fncache' in requirements:
+ > instrumentfncachestore(store, ui)
+ > return store
+ > extensions.wrapfunction(store, 'store', wrapstore)
+ >
+ > def instrumentfncachestore(fncachestore, ui):
+ > class instrumentedfncache(type(fncachestore.fncache)):
+ > def _load(self):
+ > ui.warn('fncache load triggered!\n')
+ > super(instrumentedfncache, self)._load()
+ > fncachestore.fncache.__class__ = instrumentedfncache
+ > EOF
+
+ $ fncachextpath=`pwd`/fncacheloadwarn.py
+ $ hg init nofncacheload
+ $ cd nofncacheload
+ $ printf "[extensions]\nfncacheloadwarn=$fncachextpath\n" >> .hg/hgrc
+
+A new file should trigger a load, as we'd want to update the fncache set in that case:
+
+ $ touch foo
+ $ hg ci -qAm foo
+ fncache load triggered!
+
+But modifying that file should not:
+
+ $ echo bar >> foo
+ $ hg ci -qm foo
+
+If a transaction has been aborted, the zero-size truncated index file will
+not prevent the fncache from being loaded; rather than actually abort
+a transaction, we simulate the situation by creating a zero-size index file:
+
+ $ touch .hg/store/data/bar.i
+ $ touch bar
+ $ hg ci -qAm bar
+ fncache load triggered!
+
+Unbundling should follow the same rules; existing files should not cause a load:
+
+ $ hg clone -q . tobundle
+ $ echo 'new line' > tobundle/bar
+ $ hg -R tobundle ci -qm bar
+ $ hg -R tobundle bundle -q barupdated.hg
+ $ hg unbundle -q barupdated.hg
+
+but adding new files should:
+
+ $ touch tobundle/newfile
+ $ hg -R tobundle ci -qAm newfile
+ $ hg -R tobundle bundle -q newfile.hg
+ $ hg unbundle -q newfile.hg
+ fncache load triggered!
+
+ $ cd ..