[PATCH] fncache: avoid loading the filename cache when not actually modifying it

Martijn Pieters mj at zopatista.com
Wed Jul 11 13:37:26 UTC 2018


# HG changeset patch
# User Martijn Pieters <mj at zopatista.com>
# Date 1531315693 -3600
#      Wed Jul 11 14:28:13 2018 +0100
# Node ID 39bc6540388e79da1a670fcd382aa77b8aa27224
# Parent  4d5fb4062f0bb159230062701461fa6cab9b539b
# EXP-Topic fncache_prevent_load_when_exists
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.

diff -r 4d5fb4062f0b -r 39bc6540388e mercurial/store.py
--- a/mercurial/store.py	Thu Mar 15 17:37:03 2018 +0530
+++ 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:
diff -r 4d5fb4062f0b -r 39bc6540388e tests/test-fncache.t
--- a/tests/test-fncache.t	Thu Mar 15 17:37:03 2018 +0530
+++ 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 ..


More information about the Mercurial-devel mailing list