All of lore.kernel.org
 help / color / mirror / Atom feed
From: Junio C Hamano <gitster@pobox.com>
To: Miklos Vajna <vmiklos@collabora.com>
Cc: "SZEDER Gábor" <szeder.dev@gmail.com>,
	"Patrick Steinhardt" <ps@pks.im>,
	git@vger.kernel.org
Subject: Re: [PATCH] log: speed up --follow following renames for non-linear history
Date: Thu, 15 Oct 2026 09:57:39 -0700	[thread overview]
Message-ID: <xmqq8q3yq4u4.fsf@gitster.g> (raw)
In-Reply-To: <atDnyzC_2oaLF0Wo@collabora.com> (Miklos Vajna's message of "Thu, 15 Oct 2026 16:48:43 +0200")

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));

  reply	other threads:[~2026-10-15 16:57 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
2026-10-14  6:16 ` Performance regression in `git log --follow` Miklos Vajna

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=xmqq8q3yq4u4.fsf@gitster.g \
    --to=gitster@pobox.com \
    --cc=git@vger.kernel.org \
    --cc=ps@pks.im \
    --cc=szeder.dev@gmail.com \
    --cc=vmiklos@collabora.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.