changeset 30446:b324b4e431e5

posix: give checkexec a fast path; keep the check files and test read only Before, Mercurial would create a new temporary file every time, stat it, change its exec mode, stat it again, and delete it. Most of this dance was done to handle the rare and not-so-essential case of VFAT mounts on unix. The cost of that was paid by the much more common and important case of using normal file systems. Instead, try to create and preserve .hg/cache/checkisexec and .hg/cache/checknoexec with and without exec flag set. If the files exist and have correct exec flags set, we can conclude that that file system supports the exec flag. Best case, the whole exec check can thus be done with two stat calls. Worst case, we delete the wrong files and check as usual. That will be because temporary loss of exec bit or on file systems without support for the exec bit. In that case we check as we did before, with the additional overhead of one extra stat call. It is possible that this different test algorithm in some cases on odd file systems will give different behaviour. Again, I think it will be rare and special cases and I think it is worth the risk. test-clone.t happens to show the situation where checkisexec is left behind from the old style check, while checknoexec only will be created next time a exec check will be performed.
author Mads Kiilerich <madski@unity3d.com>
date Wed, 14 Jan 2015 01:15:26 +0100
parents 1ce4c2062ab0
children 0d87b1caed92
files mercurial/posix.py tests/test-clone.t tests/test-hardlinks.t tests/test-tags.t
diffstat 4 files changed, 54 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/posix.py	Wed Jan 14 01:15:26 2015 +0100
+++ b/mercurial/posix.py	Wed Jan 14 01:15:26 2015 +0100
@@ -161,18 +161,55 @@
     try:
         EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
         cachedir = os.path.join(path, '.hg', 'cache')
-        if not os.path.isdir(cachedir):
-            cachedir = path
-        fh, fn = tempfile.mkstemp(dir=cachedir, prefix='hg-checkexec-')
+        if os.path.isdir(cachedir):
+            checkisexec = os.path.join(cachedir, 'checkisexec')
+            checknoexec = os.path.join(cachedir, 'checknoexec')
+
+            try:
+                m = os.stat(checkisexec).st_mode
+            except OSError as e:
+                if e.errno != errno.ENOENT:
+                    raise
+                # checkisexec does not exist - fall through ...
+            else:
+                # checkisexec exists, check if it actually is exec
+                if m & EXECFLAGS != 0:
+                    # ensure checkisexec exists, check it isn't exec
+                    try:
+                        m = os.stat(checknoexec).st_mode
+                    except OSError as e:
+                        if e.errno != errno.ENOENT:
+                            raise
+                        file(checknoexec, 'w').close() # might fail
+                        m = os.stat(checknoexec).st_mode
+                    if m & EXECFLAGS == 0:
+                        # check-exec is exec and check-no-exec is not exec
+                        return True
+                    # checknoexec exists but is exec - delete it
+                    os.unlink(checknoexec)
+                # checkisexec exists but is not exec - delete it
+                os.unlink(checkisexec)
+
+            # check using one file, leave it as checkisexec
+            checkdir = cachedir
+        else:
+            # check directly in path and don't leave checkisexec behind
+            checkdir = path
+            checkisexec = None
+        fh, fn = tempfile.mkstemp(dir=checkdir, prefix='hg-checkexec-')
         try:
             os.close(fh)
             m = os.stat(fn).st_mode
-            if m & EXECFLAGS:
-                return False
-            os.chmod(fn, m & 0o777 | EXECFLAGS)
-            return os.stat(fn).st_mode & EXECFLAGS
+            if m & EXECFLAGS == 0:
+                os.chmod(fn, m & 0o777 | EXECFLAGS)
+                if os.stat(fn).st_mode & EXECFLAGS != 0:
+                    if checkisexec is not None:
+                        os.rename(fn, checkisexec)
+                        fn = None
+                    return True
         finally:
-            os.unlink(fn)
+            if fn is not None:
+                os.unlink(fn)
     except (IOError, OSError):
         # we don't care, the user probably won't be able to commit anyway
         return False
--- a/tests/test-clone.t	Wed Jan 14 01:15:26 2015 +0100
+++ b/tests/test-clone.t	Wed Jan 14 01:15:26 2015 +0100
@@ -31,6 +31,8 @@
   default                       10:a7949464abda
   $ ls .hg/cache
   branch2-served
+  checkisexec
+  checknoexec
   rbc-names-v1
   rbc-revs-v1
 
@@ -45,6 +47,7 @@
 
   $ ls .hg/cache
   branch2-served
+  checkisexec
 
   $ cat a
   a
--- a/tests/test-hardlinks.t	Wed Jan 14 01:15:26 2015 +0100
+++ b/tests/test-hardlinks.t	Wed Jan 14 01:15:26 2015 +0100
@@ -211,6 +211,8 @@
   2 r4/.hg/00changelog.i
   2 r4/.hg/branch
   2 r4/.hg/cache/branch2-served
+  2 r4/.hg/cache/checkisexec
+  2 r4/.hg/cache/checknoexec
   2 r4/.hg/cache/rbc-names-v1
   2 r4/.hg/cache/rbc-revs-v1
   2 r4/.hg/dirstate
@@ -247,6 +249,8 @@
   2 r4/.hg/00changelog.i
   1 r4/.hg/branch
   2 r4/.hg/cache/branch2-served
+  2 r4/.hg/cache/checkisexec
+  2 r4/.hg/cache/checknoexec
   2 r4/.hg/cache/rbc-names-v1
   2 r4/.hg/cache/rbc-revs-v1
   1 r4/.hg/dirstate
--- a/tests/test-tags.t	Wed Jan 14 01:15:26 2015 +0100
+++ b/tests/test-tags.t	Wed Jan 14 01:15:26 2015 +0100
@@ -672,6 +672,7 @@
 
   $ ls tagsclient/.hg/cache
   branch2-served
+  checkisexec
   hgtagsfnodes1
   rbc-names-v1
   rbc-revs-v1
@@ -696,6 +697,7 @@
 
   $ ls tagsclient/.hg/cache
   branch2-served
+  checkisexec
   hgtagsfnodes1
   rbc-names-v1
   rbc-revs-v1