* Performance regression in `git log --follow`
@ 2026-10-13 13:55 Patrick Steinhardt
2026-10-13 21:24 ` SZEDER Gábor
2026-10-14 6:16 ` Performance regression in `git log --follow` Miklos Vajna
0 siblings, 2 replies; 5+ messages in thread
From: Patrick Steinhardt @ 2026-10-13 13:55 UTC (permalink / raw)
To: git; +Cc: Miklos Vajna
Hi,
a user on IRC reported a performance regression in linux.git:
$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
$ git -C linux log --stat --follow -- include/linux/rsc_table.h
I was able to reproduce the performance regression: while this command
takes roughly 33 seconds with Git 2.55, it takes almost 8 minutes with
Git 2.56.
I've bisected the regression to 304812ed33 (log: improve --follow
following renames for non-linear history, 2026-06-22). I don't have time
right now to debug any further, so I decided to just forward the report
to the mailing list. I've also Cc'd the original author of the above
commit.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: Performance regression in `git log --follow` 2026-10-13 13:55 Performance regression in `git log --follow` Patrick Steinhardt @ 2026-10-13 21:24 ` SZEDER Gábor 2026-10-15 14:48 ` [PATCH] log: speed up --follow following renames for non-linear history Miklos Vajna 2026-10-14 6:16 ` Performance regression in `git log --follow` Miklos Vajna 1 sibling, 1 reply; 5+ messages in thread From: SZEDER Gábor @ 2026-10-13 21:24 UTC (permalink / raw) To: Patrick Steinhardt; +Cc: git, Miklos Vajna On Tue, Oct 13, 2026 at 03:55:01PM +0200, Patrick Steinhardt wrote: > Hi, > > a user on IRC reported a performance regression in linux.git: > > $ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git > $ git -C linux log --stat --follow -- include/linux/rsc_table.h > > I was able to reproduce the performance regression: while this command > takes roughly 33 seconds with Git 2.55, it takes almost 8 minutes with > Git 2.56. > > I've bisected the regression to 304812ed33 (log: improve --follow > following renames for non-linear history, 2026-06-22). I don't have time > right now to debug any further, so I decided to just forward the report > to the mailing list. I've also Cc'd the original author of the above > commit. The performance regression is clearly noticable in the git repository, though the slowdown is not as pronounced as in linux.git: Benchmark 1: ./304812ed33^/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh Time (mean ± σ): 3.275 s ± 0.022 s [User: 3.157 s, System: 0.116 s] Range (min … max): 3.240 s … 3.313 s 10 runs Benchmark 2: ./304812ed33/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh Time (mean ± σ): 8.825 s ± 0.029 s [User: 8.554 s, System: 0.264 s] Range (min … max): 8.769 s … 8.864 s 10 runs Summary './304812ed33^/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh' ran 2.69 ± 0.02 times faster than './304812ed33/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh' That's 5.5 seconds of runtime difference, and with a bit of trace performance hack[1] it turns out that most of it is spent in a call to propagate_follow_pathspec_to_parent() to compare the current commit with its second...N-th parent: $ GIT_TRACE_PERFORMANCE=1 ./304812ed33/bin/git --no-pager log --follow --oneline v2.56.0 t/valgrind/valgrind.sh >/dev/null 21:59:30.465592 read-cache.c:2364 performance: 0.000626524 s: read cache .git/index 21:59:39.246697 log-tree.c:1158 performance: 1.374980154 s: first parent propagate_follow_pathspec_to_parent() 21:59:39.247164 log-tree.c:1159 performance: 4.448692273 s: N-th parents propagate_follow_pathspec_to_parent() 21:59:39.247192 trace.c:416 performance: 8.799236172 s: git command: ./304812ed33/bin/git --no-pager log --follow --oneline v2.56.0 t/valgrind/valgrind.sh Interestingly, although line-level log performs its own rename tracking, it's still notably faster than "--follow" even before this perf regression (that "-L1,40" means "track the whole file"): Benchmark 1: ./304812ed33^/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh Time (mean ± σ): 3.260 s ± 0.019 s [User: 3.140 s, System: 0.118 s] Range (min … max): 3.219 s … 3.284 s 10 runs Benchmark 2: ./304812ed33^/bin/git --no-pager log --oneline --no-patch -L1,40:t/valgrind/valgrind.sh v2.56.0 Time (mean ± σ): 1.454 s ± 0.014 s [User: 1.369 s, System: 0.084 s] Range (min … max): 1.441 s … 1.481 s 10 runs Summary './304812ed33^/bin/git --no-pager log --oneline --no-patch -L1,40:t/valgrind/valgrind.sh v2.56.0' ran 2.24 ± 0.02 times faster than './304812ed33^/bin/git --no-pager log --oneline --no-patch --follow v2.56.0 t/valgrind/valgrind.sh' The reason for this might be: - If line-level log finds that the interesting path is not modified between a merge commit and its first parent, then it doesn't perform a diff between that merge commit and its second..N-th parent. "--follow" seems to performs a diff between a merge commit and all its parents. - Line-level log checks for renames only when the path is not present in the parent commit. "--follow" seems to always look for renames. [1] It really is just a hack: diff --git a/log-tree.c b/log-tree.c index 83a3c4bf9b..fd4824fd3e 100644 --- a/log-tree.c +++ b/log-tree.c @@ -34,6 +34,7 @@ #include "wildmatch.h" #include "write-or-die.h" #include "pager.h" +#include "trace.h" static struct decoration name_decoration = { "object names" }; static int decoration_loaded; @@ -1144,6 +1145,8 @@ static void free_follow_pathspec_slot(char **slot) FREE_AND_NULL(*slot); } +static uint64_t t_first = 0, t_nth = 0; + void release_follow_pathspec_slab(struct rev_info *opt) { if (!opt->follow_pathspec_slab) @@ -1151,6 +1154,9 @@ void release_follow_pathspec_slab(struct rev_info *opt) deep_clear_follow_pathspec_slab(opt->follow_pathspec_slab, free_follow_pathspec_slot); FREE_AND_NULL(opt->follow_pathspec_slab); + + trace_performance(t_first, "first parent propagate_follow_pathspec_to_parent()"); + trace_performance(t_nth, "N-th parents propagate_follow_pathspec_to_parent()"); } /* Compute a path to follow in parent, if there is one */ @@ -1304,9 +1310,18 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) struct commit_list *parents = get_saved_parents(opt, commit); if (parents && parents->next) { struct commit_list *p; - for (p = parents; p; p = p->next) + for (p = parents; p; p = p->next) { + if (p == parents) + t_first -= getnanotime(); + else + t_nth -= getnanotime(); propagate_follow_pathspec_to_parent(opt, commit, p->item); + if (p == parents) + t_first += getnanotime(); + else + t_nth += getnanotime(); + } } else if (parents) { remember_follow_pathspec(opt, parents->item, pathspec_single_path(&opt->diffopt.pathspec)); ^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH] log: speed up --follow following renames for non-linear history 2026-10-13 21:24 ` SZEDER Gábor @ 2026-10-15 14:48 ` Miklos Vajna 2026-10-15 16:57 ` Junio C Hamano 0 siblings, 1 reply; 5+ messages in thread From: Miklos Vajna @ 2026-10-15 14:48 UTC (permalink / raw) To: SZEDER Gábor; +Cc: Patrick Steinhardt, git Test the performance of 'git log --stat --follow -- include/linux/rsc_table.h' on linux.git, it used to take about 28 seconds, and since commit 304812ed33 (log: improve --follow following renames for non-linear history, 2026-06-22), it takes 6m43s. What happens is that propagate_follow_pathspec_to_parent() does a somewhat expensive rename detection of the current commit vs all of its parents, and this slows down 'git log --follow'. Fix the problem by introducing two shortcuts: 1) Only do that when the path is added relative to a parent and 2) If the path is the same as in one of the parents, then just follow that history These shortcuts keep all the uses-cases in t4219-log-follow-merge.sh working, but now the above use case takes 29s, close to the original cost. Helped-by: SZEDER Gábor <szeder.dev@gmail.com> Signed-off-by: Miklos Vajna <vmiklos@collabora.com> --- Hi Gabor, Patrick, Here is my suggested speed-up patch. I think it's correct to take these two shortcuts since the original use cases continue to work, and then we're almost back to the original performance. Let me know what you think; if you don't see problems with it, then a bit later I would send the patch to Junio. Thanks, Miklos log-tree.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/log-tree.c b/log-tree.c index 83a3c4bf9b..0d7bf001f0 100644 --- a/log-tree.c +++ b/log-tree.c @@ -1180,6 +1180,74 @@ static void propagate_follow_pathspec_to_parent(struct rev_info *opt, diff_free(&diff_opts); } +enum follow_path_change { + FOLLOW_PATH_SAME, + FOLLOW_PATH_ADDED, + FOLLOW_PATH_OTHER, +}; + +/* How does the followed path change from parent to commit? */ +static enum follow_path_change get_follow_path_change(struct rev_info *opt, + struct commit *commit, + struct commit *parent) +{ + struct diff_options diff_opts; + enum follow_path_change ret = FOLLOW_PATH_OTHER; + + parse_commit_or_die(parent); + repo_diff_setup(opt->diffopt.repo, &diff_opts); + copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec); + diff_opts.flags.recursive = 1; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_setup_done(&diff_opts); + diff_tree_oid(get_commit_tree_oid(parent), + get_commit_tree_oid(commit), + "", &diff_opts); + + if (!diff_queued_diff.nr) + ret = FOLLOW_PATH_SAME; + else if (diff_queued_diff.nr == 1 && + !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one)) + ret = FOLLOW_PATH_ADDED; + + diff_queue_clear(&diff_queued_diff); + diff_free(&diff_opts); + return ret; +} + +/* Compute a path to follow in each parent of a merge */ +static void propagate_follow_pathspec_to_parents(struct rev_info *opt, + struct commit *commit, + struct commit_list *parents) +{ + const char *path = pathspec_single_path(&opt->diffopt.pathspec); + struct commit_list *p; + enum follow_path_change *changes; + int nr = commit_list_count(parents), i; + int has_same = 0; + + if (!path) + return; + + ALLOC_ARRAY(changes, nr); + for (p = parents, i = 0; p; p = p->next, i++) { + changes[i] = get_follow_path_change(opt, commit, p->item); + if (changes[i] == FOLLOW_PATH_SAME) { + has_same = 1; + break; + } + } + + for (p = parents, i = 0; p; p = p->next, i++) { + if (!has_same && changes[i] == FOLLOW_PATH_ADDED) + /* Never the same, added here: expensive rename detection */ + propagate_follow_pathspec_to_parent(opt, commit, p->item); + else + remember_follow_pathspec(opt, p->item, path); + } + free(changes); +} + /* * Show the diff of a commit. * @@ -1303,10 +1371,8 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) if (opt->diffopt.flags.follow_renames) { struct commit_list *parents = get_saved_parents(opt, commit); if (parents && parents->next) { - struct commit_list *p; - for (p = parents; p; p = p->next) - propagate_follow_pathspec_to_parent(opt, commit, - p->item); + propagate_follow_pathspec_to_parents(opt, commit, + parents); } else if (parents) { remember_follow_pathspec(opt, parents->item, pathspec_single_path(&opt->diffopt.pathspec)); -- 2.56.0 ^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] log: speed up --follow following renames for non-linear history 2026-10-15 14:48 ` [PATCH] log: speed up --follow following renames for non-linear history Miklos Vajna @ 2026-10-15 16:57 ` Junio C Hamano 0 siblings, 0 replies; 5+ messages in thread From: Junio C Hamano @ 2026-10-15 16:57 UTC (permalink / raw) To: Miklos Vajna; +Cc: SZEDER Gábor, Patrick Steinhardt, git Miklos Vajna <vmiklos@collabora.com> writes: > Test the performance of 'git log --stat --follow -- > include/linux/rsc_table.h' on linux.git, it used to take about 28 > seconds, and since commit 304812ed33 (log: improve --follow following > renames for non-linear history, 2026-06-22), it takes 6m43s. > > What happens is that propagate_follow_pathspec_to_parent() does a > somewhat expensive rename detection of the current commit vs all of its > parents, and this slows down 'git log --follow'. > > Fix the problem by introducing two shortcuts: > 1) Only do that when the path is added relative to a parent and > 2) If the path is the same as in one of the parents, then just follow > that history > > These shortcuts keep all the uses-cases in t4219-log-follow-merge.sh > working, but now the above use case takes 29s, close to the original > cost. The original follow-renames implementation kicked in only when the path being followed did not exist in the parent. log_tree_commit() calls log_tree_diff() to show the diff for the current commit, which calls diff_tree_oid(), which checks with diff_might_be_rename() to see if the path we have been following is missing in the parent and only then runs try_to_follow_renames(), which is expensive, to find out where it came from. It avoided the expensive second diff when we know the path we have been following existed in the parent. But reading log_tree_commit() in today's code, it seems that propagate_follow_pathspec_to_parent() is called, after we have ALREADY spent cycles to run the log_tree_diff() and flushed its output. The precious clue that tells you if the path you have been following is shared with the parent has already been flushed at that point. Perhaps inside log_tree_diff_flush() while we still have the list of filepairs in diff_queue is where you should be able to see if any of the paths you have been following appeared anew relative to the parent(s) and to decide if you must adjust the list of paths you follow for renames, instead of calling the diff machinery all the time from propagate_follow_pathspec_to_parent() unconditionally. I am not sure if it may be cleaner to first rip out all the change introduced in 304812ed33 (log: improve --follow following renames for non-linear history, 2026-06-22) and then rethink the approach, instead of piling more code with additional patches. I dunno. Thanks. > log-tree.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++--- > 1 file changed, 70 insertions(+), 4 deletions(-) > > diff --git a/log-tree.c b/log-tree.c > index 83a3c4bf9b..0d7bf001f0 100644 > --- a/log-tree.c > +++ b/log-tree.c > @@ -1180,6 +1180,74 @@ static void propagate_follow_pathspec_to_parent(struct rev_info *opt, > diff_free(&diff_opts); > } > > +enum follow_path_change { > + FOLLOW_PATH_SAME, > + FOLLOW_PATH_ADDED, > + FOLLOW_PATH_OTHER, > +}; > + > +/* How does the followed path change from parent to commit? */ > +static enum follow_path_change get_follow_path_change(struct rev_info *opt, > + struct commit *commit, > + struct commit *parent) > +{ > + struct diff_options diff_opts; > + enum follow_path_change ret = FOLLOW_PATH_OTHER; > + > + parse_commit_or_die(parent); > + repo_diff_setup(opt->diffopt.repo, &diff_opts); > + copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec); > + diff_opts.flags.recursive = 1; > + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; > + diff_setup_done(&diff_opts); > + diff_tree_oid(get_commit_tree_oid(parent), > + get_commit_tree_oid(commit), > + "", &diff_opts); > + > + if (!diff_queued_diff.nr) > + ret = FOLLOW_PATH_SAME; > + else if (diff_queued_diff.nr == 1 && > + !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one)) > + ret = FOLLOW_PATH_ADDED; > + > + diff_queue_clear(&diff_queued_diff); > + diff_free(&diff_opts); > + return ret; > +} > + > +/* Compute a path to follow in each parent of a merge */ > +static void propagate_follow_pathspec_to_parents(struct rev_info *opt, > + struct commit *commit, > + struct commit_list *parents) > +{ > + const char *path = pathspec_single_path(&opt->diffopt.pathspec); > + struct commit_list *p; > + enum follow_path_change *changes; > + int nr = commit_list_count(parents), i; > + int has_same = 0; > + > + if (!path) > + return; > + > + ALLOC_ARRAY(changes, nr); > + for (p = parents, i = 0; p; p = p->next, i++) { > + changes[i] = get_follow_path_change(opt, commit, p->item); > + if (changes[i] == FOLLOW_PATH_SAME) { > + has_same = 1; > + break; > + } > + } > + > + for (p = parents, i = 0; p; p = p->next, i++) { > + if (!has_same && changes[i] == FOLLOW_PATH_ADDED) > + /* Never the same, added here: expensive rename detection */ > + propagate_follow_pathspec_to_parent(opt, commit, p->item); > + else > + remember_follow_pathspec(opt, p->item, path); > + } > + free(changes); > +} > + > /* > * Show the diff of a commit. > * > @@ -1303,10 +1371,8 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) > if (opt->diffopt.flags.follow_renames) { > struct commit_list *parents = get_saved_parents(opt, commit); > if (parents && parents->next) { > - struct commit_list *p; > - for (p = parents; p; p = p->next) > - propagate_follow_pathspec_to_parent(opt, commit, > - p->item); > + propagate_follow_pathspec_to_parents(opt, commit, > + parents); > } else if (parents) { > remember_follow_pathspec(opt, parents->item, > pathspec_single_path(&opt->diffopt.pathspec)); ^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: Performance regression in `git log --follow` 2026-10-13 13:55 Performance regression in `git log --follow` Patrick Steinhardt 2026-10-13 21:24 ` SZEDER Gábor @ 2026-10-14 6:16 ` Miklos Vajna 1 sibling, 0 replies; 5+ messages in thread From: Miklos Vajna @ 2026-10-14 6:16 UTC (permalink / raw) To: Patrick Steinhardt; +Cc: git Hi Patrick, On Tue, Oct 13, 2026 at 03:55:01PM +0200, Patrick Steinhardt <ps@pks.im> wrote: > I've bisected the regression to 304812ed33 (log: improve --follow > following renames for non-linear history, 2026-06-22). I don't have time > right now to debug any further, so I decided to just forward the report > to the mailing list. I've also Cc'd the original author of the above > commit. Thanks for the report. To some extent, this is expected: previously log --follow had a design issue, as described at <https://lore.kernel.org/git/xmqqjysz7r41.fsf@gitster.g/>. Now log --follow finds more commits has it traverses non-linear history, which takes more time. Let's see if we can find some creative solution that keeps the new design (storing / restoring the path to follow as you traverse non-linear history) and at the same time improves performance for your use-case. Quite possibly my patch is/was naive and we can avoid some not needed work while still correctly following a path across renames. I see Gabor already commented in the thread and same some ideas, but I haven't read those yet. Regards, Miklos ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-15 16:57 UTC | newest] Thread overview: 5+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-10-13 13:55 Performance regression in `git log --follow` Patrick Steinhardt 2026-10-13 21:24 ` SZEDER Gábor 2026-10-15 14:48 ` [PATCH] log: speed up --follow following renames for non-linear history Miklos Vajna 2026-10-15 16:57 ` Junio C Hamano 2026-10-14 6:16 ` Performance regression in `git log --follow` Miklos Vajna
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox