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 | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 95 insertions(+), 4 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8672d07..4396cb3 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;
@@ -802,6 +806,87 @@ static void orphaned_commit_warning(struct commit *old, 
struct commit *new)
        free(refs.objects);
 }
 
+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 switch_branches(const struct checkout_opts *opts,
                           struct branch_info *new)
 {
@@ -827,10 +912,16 @@ static int switch_branches(const struct checkout_opts 
*opts,
                parse_commit_or_die(new->commit);
        }
 
-       ret = merge_working_tree(opts, &old, new, &writeout_error);
-       if (ret) {
-               free(path_to_free);
-               return ret;
+       /*
+        * Optimize the performance of "git checkout foo" by skipping the call
+        * to merge_working_tree where possible.
+        */
+       if (needs_working_tree_merge(opts, &old, new)) {
+               ret = merge_working_tree(opts, &old, new, &writeout_error);
+               if (ret) {
+                       free(path_to_free);
+                       return ret;
+               }
        }
 
        if (!opts->quiet && !old.path && old.commit && new->commit != 
old.commit)
-- 
2.10.0.windows.1

Reply via email to