Git development
 help / color / mirror / Atom feed
* 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

* 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

* [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

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