Teach git to avoid unnecessary merge during trivial checkout.  When
running 'git checkout -b foo' git follows a common code path through
the expensive merge_working_tree even when it is unnecessary.  As a
result, 95% of the time is spent in merge_working_tree doing the 2-way
merge between the new and old commit trees that is unneeded.

The time breakdown is as follows:

    merge_working_tree <-- 95%
        unpack_trees <-- 80%
            traverse_trees <-- 50%
            cache_tree_update <-- 17%
            mark_new_skip_worktree <-- 10%

With a large repo, this cost is pronounced.  Using "git checkout -b r"
to create and switch to a new branch costs 166 seconds (all times worst
case with a cold file system cache).

git.c:406               trace: built-in: git 'checkout' '-b' 'r'
read-cache.c:1667       performance: 17.442926555 s: read_index_from
name-hash.c:128         performance: 2.912145231 s: lazy_init_name_hash
read-cache.c:2208       performance: 4.387713335 s: write_locked_index
trace.c:420             performance: 166.458921289 s: git command:
                                        'c:\Users\benpeart\bin\git.exe' 
'checkout' '-b' 'r'
Switched to a new branch 'r'

By adding a test to skip the unnecessary call to merge_working_tree in
this case reduces the cost to 16 seconds.

git.c:406               trace: built-in: git 'checkout' '-b' 's'
read-cache.c:1667       performance: 16.100742476 s: read_index_from
trace.c:420             performance: 16.461547867 s: git command: 
'c:\Users\benpeart\bin\git.exe' 'checkout' '-b' 's'
Switched to a new branch 's'

Signed-off-by: Ben Peart <benpe...@microsoft.com>
---
 builtin/checkout.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8672d07..8b2f428 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -38,6 +38,10 @@ struct checkout_opts {
        int ignore_skipworktree;
        int ignore_other_worktrees;
        int show_progress;
+       /*
+        * If new checkout options are added, needs_working_tree_merge
+        * should be updated accordingly.
+        */
 
        const char *new_branch;
        const char *new_branch_force;
@@ -460,11 +464,99 @@ static void setup_branch_path(struct branch_info *branch)
        branch->path = strbuf_detach(&buf, NULL);
 }
 
+static int needs_working_tree_merge(const struct checkout_opts *opts,
+       const struct branch_info *old,
+       const struct branch_info *new)
+{
+       /*
+        * We must do the merge if we are actually moving to a new
+        * commit tree.
+        */
+       if (!old->commit || !new->commit ||
+               oidcmp(&old->commit->tree->object.oid, 
&new->commit->tree->object.oid))
+               return 1;
+
+       /*
+        * opts->patch_mode cannot be used with switching branches so is
+        * not tested here
+        */
+
+       /*
+        * opts->quiet only impacts output so doesn't require a merge
+        */
+
+       /*
+        * Honor the explicit request for a three-way merge or to throw away
+        * local changes
+        */
+       if (opts->merge || opts->force)
+               return 1;
+
+       /*
+        * Checking out the requested commit may require updating the working
+        * directory and index, let the merge handle it.
+        */
+       if (opts->force_detach)
+               return 1;
+
+       /*
+        * opts->writeout_stage cannot be used with switching branches so is
+        * not tested here
+        */
+
+       /*
+        * Honor the explicit ignore requests
+        */
+       if (!opts->overwrite_ignore || opts->ignore_skipworktree ||
+               opts->ignore_other_worktrees)
+               return 1;
+
+       /*
+        * opts->show_progress only impacts output so doesn't require a merge
+        */
+
+       /*
+        * If we're not creating a new branch, by definition we're changing
+        * the existing one so need to do the merge
+        */
+       if (!opts->new_branch)
+               return 1;
+
+       /*
+        * new_branch_force is defined to "create/reset and checkout a branch"
+        * so needs to go through the merge to do the reset
+        */
+       if (opts->new_branch_force)
+               return 1;
+
+       /*
+        * A new orphaned branch requrires the index and the working tree to be
+        * adjusted to <start_point>
+        */
+       if (opts->new_orphan_branch)
+               return 1;
+
+       /*
+        * Remaining variables are not checkout options but used to track state
+        * that doesn't trigger the need for a merge.
+        */
+
+       return 0;
+}
+
 static int merge_working_tree(const struct checkout_opts *opts,
                              struct branch_info *old,
                              struct branch_info *new,
                              int *writeout_error)
 {
+       /*
+        * Optimize the performance of "git checkout -b foo" by avoiding
+        * the expensive merge, index and working directory updates if they
+        * are not needed.
+        */
+       if (!needs_working_tree_merge(opts, old, new))
+               return 0;
+
        int ret;
        struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file));
 
-- 
2.10.0.windows.1

Reply via email to