git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [RFC/PATCH 1/4] tag: speed up --contains calculation

From
Jonathan Nieder <jrnieder@gmail.com>
Date
Oct 13, 2010, 22:07 UTC
Message-ID
<20101013220753.GA8674@burratino>
In-Reply-To
<20100705123335.GA25699@sigill.intra.peff.net>
Hi,
Jeff King wrote:
> When we want to know if commit A contains commit B (or any
> one of a set of commits, B through Z),

Let's revive this old thread. Sorry for the long silence (the last time this topic was visited was a couple months ago [1] afaict).

Show 6 quoted lines
> Instead, let's leverage the fact that we are going to use
> the same --contains list for each tag, and mark areas of the
> commit graph is definitely containing those commits, or
> definitely not containing those commits. Later tags can then
> stop traversing as soon as they see a previously calculated
> answer.
Makes sense.  And the resulting timings are exciting.
Show 11 quoted lines
> ---
> Note that this is a depth first search, whereas we generally traverse in
> a more breadth-first way. So it can actually make things slightly slower
> than the current merge-base code, if:
> 
>   1. You don't have any merge base calculations that involve going very
>      far back in history.
> 
>   2. We depth-first down the wrong side of a merge.
> 
> However, in the usual cases, I think it will perform much better.
Nit: Wouldn't this (or a summary of it) belong in the log message, too,
to avoid headscratching when a person tries to bisect a performance
regression down the line?
>   1. This can probably be a one-liner change to use in "git branch
>      --contains", as well. I didn't measure it, as that already tends to
>      be reasonably fast.

These days "branch -r --contains" has been _sloow_ for me. So once this is tuned, that wouldn't be a bad idea, methinks.

>   2. It uses commit marks, which means it doesn't behave well with other
>      traversals. I have no idea if I should be using alternate marks or
>      not.

This is what Junio was referring to with "the use of TMP_MARK smelled somewhat bad to me", right?

>   3. We never clear the marks, or check them against the "want" list.
This too, I suppose.
> So
>      we just assume that repeated calls to contains will have the same
>      "want" list.

As long as this is advertised, I think it is fine. If someone needs to use --contains for multiple purposes in a builtin, that can be dealt with then (maybe with a separate function to reset the marks).

Maybe it would make sense to keep this static in builtin/tag.c for now, and defer cleaning up the interface to a separate patch.

> --- a/commit.c
> +++ b/commit.c

Now the meat of the patch. git has a few algorithms for checking if one commit is an ancestor of another.

is_descendant_of::
	1. find merge bases.  This is breadth-first search in
	   date order, but it walks through all ancestors as far
	   as I can tell.
	2. make them independent (seems pointless...).
	3. check if ancestor is one of them.
get_revision after setting UNINTERESTING (i.e., rev-list -1 ^tag cmit)::
	1. find interesting and uninteresting commits.  This is
	   breadth-first search in date order.
	2. when the first interesting commit is found, emit it
	   and stop.

Neither is optimized to check a batch of commits to find which contain the commit of interest.

join_revs of show-branch::
	1. color the tag and cmit.
	2. let colors propagate back to all their ancestors, stopping
	   propagation along a given path when a commit has all colors.
	   This is breadth-first traversal in date order.
	3. for each commit with all colors, propagate that knowledge
	   to all parents we've encountered before.
	4. check what colors cmit has.
limit_to_ancestry::
	1. compute rev-list ^cmit tag in the usual way.
	2. reverse the revision list.
	3. color cmit with TMP_MARK and let that color permeate
	   up the backwards revision list.
	4. repeat! until no progress is made.
	4. check what color tag has.
None of these seem to tolerate timestamp skew. :(
Am I understanding correctly?

If so, just making is_descendant_of less stupid (e.g., not traversing all history) would presumably help a lot. Another approach would be to try the show-branch tactic repeatedly with small enough batches of tags to fit in the flag bitfield. Yet another approach would be to take the limit_to_ancestry one, but if the revision list is not topologically sorted, it could be slow.

Hopefully your approach will be even better...
> @@ -845,3 +845,45 @@ int commit_tree(const char *msg, unsigned char *tree,
[...]
Show 26 quoted lines
> +static int contains_recurse(struct commit *candidate,
> +			    const struct commit_list *want)
> +{
> +	struct commit_list *p;
> +
> +	/* was it previously marked as containing a want commit? */
> +	if (candidate->object.flags & TMP_MARK)
> +		return 1;
> +	/* or marked as not possibly containing a want commit? */
> +	if (candidate->object.flags & UNINTERESTING)
> +		return 0;
> +	/* or are we it? */
> +	if (in_commit_list(want, candidate))
> +		return 1;
> +
> +	if (parse_commit(candidate) < 0)
> +		return 0;
> +
> +	/* Otherwise recurse and mark ourselves for future traversals. */
> +	for (p = candidate->parents; p; p = p->next) {
> +		if (contains_recurse(p->item, want)) {
> +			candidate->object.flags |= TMP_MARK;
> +			return 1;
> +		}
> +	}
> +	candidate->object.flags |= UNINTERESTING;

Nice. We cache the "is wanted" result using TMP_MARK and UNINTERESTING. It cannot be slower than is_descendant_of if I understood correctly because the latter is a little crazy.

Possible unprincipled approach:
 1. Mark the wanted commits with TMP_MARK.
 2. Perform a breadth-first, date-order search starting at
    candidate to find a commit with TMP_MARK.
    a. If not found, mark the commits involved as UNINTERESTING
    b. If found, let the TMP_MARK permeate up using the
       limit_to_ancestry algorithm.
 3. Repeat with each other candidate.
 4. (If needed) walk starting at each candidate to clear the
    temporary flags.
I am not sure if that would be any faster, though.  What do you think?
[...]
> +int contains(struct commit *, const struct commit_list *);
Thanks for a pleasant read.
[1] http://thread.gmane.org/gmane.comp.version-control.git/152607/focus=152701
Previous: Jeff KingNext: Clemens Buchacher
Message 13 of 46 in “Why is "git tag --contains" so slow?”
  1. Theodore Ts'oJul 1, 2010
  2. Shawn O. PearceJul 1, 2010
  3. Sam VilainJul 3, 2010
  4. Avery PennarunJul 1, 2010
  5. tytso@mit.eduJul 1, 2010
  6. Jeff KingJul 1, 2010
  7. Jeff KingJul 1, 2010
  8. tytso@mit.eduJul 2, 2010
  9. Jeff KingJul 3, 2010
  10. tytso@mit.eduJul 4, 2010
  11. Jeff KingJul 5, 2010
  12. 1/4 tag: speed up --contains calculationJeff King, Jul 5, 2010
  13. Jonathan NiederOct 13, 2010
  14. Clemens BuchacherOct 13, 2010
  15. Ævar Arnfjörð BjarmasonFeb 23, 2011
  16. Jeff KingFeb 23, 2011
  17. 2/4 limit "contains" traversals based on commit timestampJeff King, Jul 5, 2010
  18. Jonathan NiederOct 13, 2010
  19. 3/4 default core.clockskew variable to one dayJeff King, Jul 5, 2010
  20. 4/4 name-rev: respect core.clockskewJeff King, Jul 5, 2010
  21. Jeff KingJul 5, 2010
  22. Jonathan NiederOct 14, 2010
  23. Clemens BuchacherOct 16, 2010
  24. Jeff KingOct 27, 2010
  25. Clemens BuchacherOct 28, 2010
  26. tytso@mit.eduJul 5, 2010
  27. Jeff KingJul 6, 2010
  28. Will PalmerJul 6, 2010
  29. tytso@mit.eduJul 6, 2010
  30. Jeff KingJul 8, 2010
  31. Will PalmerJul 8, 2010
  32. tytso@mit.eduJul 8, 2010
  33. Jeff KingJul 7, 2010
  34. Theodore TsoJul 8, 2010
  35. Jakub NarebskiJul 8, 2010
  36. Nicolas PitreJul 8, 2010
  37. Avery PennarunJul 8, 2010
  38. Nicolas PitreJul 8, 2010
  39. Jakub NarebskiJul 8, 2010
  40. Sverre RabbelierJul 8, 2010
  41. Nicolas PitreJul 8, 2010
  42. Nicolas PitreJul 8, 2010
  43. Jeff KingJul 8, 2010
  44. Johan HerlandJul 8, 2010
  45. Nicolas PitreJul 8, 2010
  46. Jeff KingJul 7, 2010

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.