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

Re: [PATCH 09/15] refs/packed-backend.c: implement skip lists to avoid excluded pattern(s)

From
Patrick Steinhardt <ps@pks.im>
Date
May 9, 2023, 15:15 UTC
Message-ID
<ZFpjn-vjP6H_v_KD@ncase>
In-Reply-To
<a39d1107c1f3e9fbd859a23aed72e63bbd394fa2.1683581621.git.me@ttaylorr.com>
On Mon, May 08, 2023 at 06:00:08PM -0400, Taylor Blau wrote:
Show 34 quoted lines
> When iterating through the `packed-refs` file in order to answer a query
> like:
> 
>     $ git for-each-ref --exclude=refs/__hidden__
> 
> it would be useful to avoid walking over all of the entries in
> `refs/__hidden__/*` when possible, since we know that the ref-filter
> code is going to throw them away anyways.
> 
> In certain circumstances, doing so is possible. The algorithm for doing
> so is as follows:
> 
>   - For each excluded pattern, find the first record that matches it,
>     and the first pattern that *doesn't* match it (i.e. the location
>     you'd next want to consider when excluding that pattern).
> 
>   - Sort the patterns by their starting location within the
>     `packed-refs` file.
> 
>   - Construct a skip list of regions by combining adjacent and
>     overlapping regions from the previous step.
> 
>   - When iterating through the `packed-refs` file, if `iter->pos` is
>     ever contained in one of the regions from the previous steps,
>     advance `iter->pos` past the end of that region, and continue
>     enumeration.
> 
> Note that this optimization is only possible when none of the excluded
> pattern(s) have special meta-characters in them. To see why this is the
> case, consider the exclusion pattern "refs/foo[a]". In general, in order
> to find the location of the first record that matches this pattern, we
> could only consider up to the first meta-character, "refs/foo". But this
> doesn't work, since the excluded region we'd come up with would include
> "refs/foobar", even though it is not excluded.

Is this generally true though? A naive implementation would iterate through all references and find the first reference that matches the exclusion regular exepression. From thereon we continue to iterate until we find the first entry that doesn't match. This may cause us to end up with a suboptimal skip list, but the skip list would still be valid.

As I said, this implementation would be naive as we're now forced to iterate through all references starting at the beginning. I assume that your implementation will instead use a binary search to locate the first entry that matches the exclusion pattern and the last pattern. But the way this paragraph is formulated makes it sound like this is a general fact, even though it is a fact that derives from the implementation.

I of course don't propose to change the algorithm here, but instead to clarify where this restriction actually comes from and why the tradeoff makes sense.

[snip]
Show 62 quoted lines
> @@ -929,6 +972,108 @@ static struct ref_iterator_vtable packed_ref_iterator_vtable = {
>  	.abort = packed_ref_iterator_abort
>  };
>  
> +static int skip_list_entry_cmp(const void *va, const void *vb)
> +{
> +	const struct skip_list_entry *a = va;
> +	const struct skip_list_entry *b = vb;
> +
> +	if (a->start < b->start)
> +		return -1;
> +	if (a->start > b->start)
> +		return 1;
> +	return 0;
> +}
> +
> +static int has_glob_special(const char *str)
> +{
> +	const char *p;
> +	for (p = str; *p; p++) {
> +		if (is_glob_special(*p))
> +			return 1;
> +	}
> +	return 0;
> +}
> +
> +static const char *ptr_max(const char *x, const char *y)
> +{
> +	if (x > y)
> +		return x;
> +	return y;
> +}
> +
> +static void populate_excluded_skip_list(struct packed_ref_iterator *iter,
> +					struct snapshot *snapshot,
> +					const char **excluded_patterns)
> +{
> +	size_t i, j;
> +	const char **pattern;
> +
> +	if (!excluded_patterns)
> +		return;
> +
> +	for (pattern = excluded_patterns; *pattern; pattern++) {
> +		struct skip_list_entry *e;
> +
> +		/*
> +		 * We can't feed any excludes with globs in them to the
> +		 * refs machinery.  It only understands prefix matching.
> +		 * We likewise can't even feed the string leading up to
> +		 * the first meta-character, as something like "foo[a]"
> +		 * should not exclude "foobar" (but the prefix "foo"
> +		 * would match that and mark it for exclusion).
> +		 */
> +		if (has_glob_special(*pattern))
> +			continue;
> +
> +		ALLOC_GROW(iter->skip, iter->skip_nr + 1, iter->skip_alloc);
> +
> +		e = &iter->skip[iter->skip_nr++];
> +		e->start = find_reference_location(snapshot, *pattern, 0);
> +		e->end = find_reference_location_end(snapshot, *pattern, 0);

One thing that makes this hard to reason about is that we don't explicitly handle the case where the pattern doesn't match at all. So you require a bunch of knowledge about what exactly the functions `find_reference_location()` and `find_reference_location_end()` do in that case in order to determine whether we will end up doing the right thing.

Explicitly handling this would give us some benefits:
- It makes the code more obvious.
- We can avoid an extra skip list entry for every non-matching
  pattern.
- We wouldn't have to perform binary search over the snapshot twice.
Might be I'm missing something though.
Show 39 quoted lines
> +	}
> +
> +	if (!iter->skip_nr) {
> +		/*
> +		 * Every entry in exclude_patterns has a meta-character,
> +		 * nothing to do here.
> +		 */
> +		return;
> +	}
> +
> +	QSORT(iter->skip, iter->skip_nr, skip_list_entry_cmp);
> +
> +	/*
> +	 * As an optimization, merge adjacent entries in the skip list
> +	 * to jump forwards as far as possible when entering a skipped
> +	 * region.
> +	 *
> +	 * For example, if we have two skipped regions:
> +	 *
> +	 *	[[A, B], [B, C]]
> +	 *
> +	 * we want to combine that into a single entry jumping from A to
> +	 * C.
> +	 */
> +	for (i = 1, j = 1; i < iter->skip_nr; i++) {
> +		struct skip_list_entry *ours = &iter->skip[i];
> +		struct skip_list_entry *prev = &iter->skip[i - 1];
> +
> +		if (ours->start == ours->end) {
> +			/* ignore empty regions (no matching entries) */
> +			continue;
> +		} else if (prev->end >= ours->start) {
> +			/* overlapping regions extend the previous one */
> +			prev->end = ptr_max(prev->end, ours->end);
> +		} else {
> +			/* otherwise, insert a new region */
> +			iter->skip[j++] = *ours;
> +		}
> +	}

Mh. Does this do the right thing in case we have multiple consecutive overlapping skip list entries? We always end up adjusting the immediate predecessor as we use `i - 1` to find `prev`. Shouldn't we instead start with `j = 0` and assign `prev = &iter->skip[j]`?

[snip]
Show 49 quoted lines
> diff --git a/t/t1419-exclude-refs.sh b/t/t1419-exclude-refs.sh
> new file mode 100755
> index 0000000000..da5265a5a8
> --- /dev/null
> +++ b/t/t1419-exclude-refs.sh
> @@ -0,0 +1,101 @@
> +#!/bin/sh
> +
> +test_description='test exclude_patterns functionality in main ref store'
> +
> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
> +
> +TEST_PASSES_SANITIZE_LEAK=true
> +. ./test-lib.sh
> +
> +for_each_ref__exclude () {
> +	test-tool ref-store main for-each-ref--exclude "$@" >actual.raw
> +	cut -d ' ' -f 2 actual.raw
> +}
> +
> +for_each_ref () {
> +	git for-each-ref --format='%(refname)' "$@"
> +}
> +
> +test_expect_success 'setup' '
> +	test_commit --no-tag base &&
> +	base="$(git rev-parse HEAD)" &&
> +
> +	for name in foo bar baz quux
> +	do
> +		for i in 1 2 3
> +		do
> +			echo "create refs/heads/$name/$i $base" || return 1
> +		done || return 1
> +	done >in &&
> +	echo "delete refs/heads/main" >>in &&
> +
> +	git update-ref --stdin <in &&
> +	git pack-refs --all
> +'
> +
> +test_expect_success 'for_each_ref__exclude(refs/heads/foo/)' '
> +	# region in middle
> +	for_each_ref__exclude refs/heads refs/heads/foo >actual &&
> +	for_each_ref refs/heads/bar refs/heads/baz refs/heads/quux >expect &&
> +
> +	test_cmp expect actual
> +'
Nit: it might be a bit more readable if we put the comment into the test
description instead of having an opaque description that mentions ref
names that don't have much of a meaning without reading the test itself.
Patrick
Previous: Taylor BlauNext: Taylor Blau
Message 25 of 149 in “refs: implement skip lists for packed backend”
  1. 00/15 refs: implement skip lists for packed backendTaylor Blau, May 8, 2023
  2. 01/15 refs.c: rename `ref_filter`Taylor Blau, May 8, 2023
  3. 03/15 ref-filter: clear reachable list pointers after freeingTaylor Blau, May 8, 2023
  4. 02/15 ref-filter.h: provide `REF_FILTER_INIT`Taylor Blau, May 8, 2023
  5. 04/15 ref-filter: add ref_filter_clear()Taylor Blau, May 8, 2023
  6. Junio C HamanoMay 8, 2023
  7. Taylor BlauMay 8, 2023
  8. Patrick SteinhardtMay 9, 2023
  9. Taylor BlauMay 9, 2023
  10. 05/15 ref-filter.c: parameterize match functions over patternsTaylor Blau, May 8, 2023
  11. Junio C HamanoMay 8, 2023
  12. Taylor BlauMay 9, 2023
  13. 06/15 builtin/for-each-ref.c: add `--exclude` optionTaylor Blau, May 8, 2023
  14. Junio C HamanoMay 8, 2023
  15. Taylor BlauMay 9, 2023
  16. 07/15 refs: plumb `exclude_patterns` argument throughoutTaylor Blau, May 8, 2023
  17. Patrick SteinhardtMay 9, 2023
  18. Taylor BlauMay 9, 2023
  19. 08/15 refs/packed-backend.c: refactor `find_reference_location()`Taylor Blau, May 8, 2023
  20. Junio C HamanoMay 8, 2023
  21. Taylor BlauMay 9, 2023
  22. 09/15 refs/packed-backend.c: implement skip lists to avoid excluded pattern(s)Taylor Blau, May 8, 2023
  23. Chris TorekMay 9, 2023
  24. Taylor BlauMay 9, 2023
  25. Patrick SteinhardtMay 9, 2023
  26. Taylor BlauMay 9, 2023
  27. Taylor BlauMay 9, 2023
  28. Patrick SteinhardtMay 10, 2023
  29. Junio C HamanoMay 9, 2023
  30. Taylor BlauMay 10, 2023
  31. 10/15 refs/packed-backend.c: add trace2 counters for skip listTaylor Blau, May 8, 2023
  32. 12/15 refs/packed-backend.c: ignore complicated hidden refs rulesTaylor Blau, May 8, 2023
  33. 11/15 revision.h: store hidden refs in a `strvec`Taylor Blau, May 8, 2023
  34. 13/15 refs.h: let `for_each_namespaced_ref()` take excluded patternsTaylor Blau, May 8, 2023
  35. 14/15 upload-pack.c: avoid enumerating hidden refs where possibleTaylor Blau, May 8, 2023
  36. Patrick SteinhardtMay 9, 2023
  37. Taylor BlauMay 9, 2023
  38. 15/15 builtin/receive-pack.c: avoid enumerating hidden referencesTaylor Blau, May 8, 2023
  39. 00/16 refs: implement jump lists for packed backendTaylor Blau, May 15, 2023
  40. 01/16 refs.c: rename `ref_filter`Taylor Blau, May 15, 2023
  41. 02/16 ref-filter.h: provide `REF_FILTER_INIT`Taylor Blau, May 15, 2023
  42. 03/16 ref-filter: clear reachable list pointers after freeingTaylor Blau, May 15, 2023
  43. 04/16 ref-filter: add `ref_filter_clear()`Taylor Blau, May 15, 2023
  44. 05/16 ref-filter.c: parameterize match functions over patternsTaylor Blau, May 15, 2023
  45. 06/16 builtin/for-each-ref.c: add `--exclude` optionTaylor Blau, May 15, 2023
  46. 07/16 refs: plumb `exclude_patterns` argument throughoutTaylor Blau, May 15, 2023
  47. 08/16 refs/packed-backend.c: refactor `find_reference_location()`Taylor Blau, May 15, 2023
  48. 09/16 refs/packed-backend.c: implement jump lists to avoid excluded pattern(s)Taylor Blau, May 15, 2023
  49. Patrick SteinhardtJun 6, 2023
  50. Taylor BlauJun 20, 2023
  51. 10/16 refs/packed-backend.c: add trace2 counters for jump listTaylor Blau, May 15, 2023
  52. 11/16 revision.h: store hidden refs in a `strvec`Taylor Blau, May 15, 2023
  53. Patrick SteinhardtJun 6, 2023
  54. Taylor BlauJun 20, 2023
  55. 12/16 refs/packed-backend.c: ignore complicated hidden refs rulesTaylor Blau, May 15, 2023
  56. 13/16 refs.h: let `for_each_namespaced_ref()` take excluded patternsTaylor Blau, May 15, 2023
  57. Patrick SteinhardtJun 6, 2023
  58. Taylor BlauJun 20, 2023
  59. 14/16 builtin/receive-pack.c: avoid enumerating hidden referencesTaylor Blau, May 15, 2023
  60. 15/16 upload-pack.c: avoid enumerating hidden refs where possibleTaylor Blau, May 15, 2023
  61. 16/16 ls-refs.c: avoid enumerating hidden refs where possibleTaylor Blau, May 15, 2023
  62. Patrick SteinhardtJun 6, 2023
  63. Taylor BlauJun 20, 2023
  64. 00/16 refs: implement jump lists for packed backendTaylor Blau, Jun 7, 2023
  65. 01/16 refs.c: rename `ref_filter`Taylor Blau, Jun 7, 2023
  66. Junio C HamanoJun 13, 2023
  67. 02/16 ref-filter.h: provide `REF_FILTER_INIT`Taylor Blau, Jun 7, 2023
  68. 03/16 ref-filter: clear reachable list pointers after freeingTaylor Blau, Jun 7, 2023
  69. 04/16 ref-filter: add `ref_filter_clear()`Taylor Blau, Jun 7, 2023
  70. 05/16 ref-filter.c: parameterize match functions over patternsTaylor Blau, Jun 7, 2023
  71. Junio C HamanoJun 13, 2023
  72. 06/16 builtin/for-each-ref.c: add `--exclude` optionTaylor Blau, Jun 7, 2023
  73. 07/16 refs: plumb `exclude_patterns` argument throughoutTaylor Blau, Jun 7, 2023
  74. Junio C HamanoJun 13, 2023
  75. Taylor BlauJun 20, 2023
  76. 08/16 refs/packed-backend.c: refactor `find_reference_location()`Taylor Blau, Jun 7, 2023
  77. 09/16 refs/packed-backend.c: implement jump lists to avoid excluded pattern(s)Taylor Blau, Jun 7, 2023
  78. Junio C HamanoJun 14, 2023
  79. Taylor BlauJun 20, 2023
  80. Junio C HamanoJun 20, 2023
  81. 10/16 refs/packed-backend.c: add trace2 counters for jump listTaylor Blau, Jun 7, 2023
  82. Junio C HamanoJun 14, 2023
  83. Taylor BlauJun 20, 2023
  84. 11/16 revision.h: store hidden refs in a `strvec`Taylor Blau, Jun 7, 2023
  85. 12/16 refs/packed-backend.c: ignore complicated hidden refs rulesTaylor Blau, Jun 7, 2023
  86. Junio C HamanoJun 14, 2023
  87. 13/16 refs.h: let `for_each_namespaced_ref()` take excluded patternsTaylor Blau, Jun 7, 2023
  88. 14/16 builtin/receive-pack.c: avoid enumerating hidden referencesTaylor Blau, Jun 7, 2023
  89. 15/16 upload-pack.c: avoid enumerating hidden refs where possibleTaylor Blau, Jun 7, 2023
  90. 16/16 ls-refs.c: avoid enumerating hidden refs where possibleTaylor Blau, Jun 7, 2023
  91. Junio C HamanoJun 12, 2023
  92. 00/16 refs: implement jump lists for packed backendTaylor Blau, Jun 20, 2023
  93. 01/16 refs.c: rename `ref_filter`Taylor Blau, Jun 20, 2023
  94. Jeff KingJul 3, 2023
  95. 02/16 ref-filter.h: provide `REF_FILTER_INIT`Taylor Blau, Jun 20, 2023
  96. Jeff KingJul 3, 2023
  97. Taylor BlauJul 3, 2023
  98. 03/16 ref-filter: clear reachable list pointers after freeingTaylor Blau, Jun 20, 2023
  99. Jeff KingJul 3, 2023
  100. 04/16 ref-filter: add `ref_filter_clear()`Taylor Blau, Jun 20, 2023
  101. Jeff KingJul 3, 2023
  102. Taylor BlauJul 3, 2023
  103. Jeff KingJul 3, 2023
  104. 05/16 ref-filter.c: parameterize match functions over patternsTaylor Blau, Jun 20, 2023
  105. Jeff KingJul 3, 2023
  106. Taylor BlauJul 3, 2023
  107. Taylor BlauJul 3, 2023
  108. Jeff KingJul 3, 2023
  109. 06/16 builtin/for-each-ref.c: add `--exclude` optionTaylor Blau, Jun 20, 2023
  110. 07/16 refs: plumb `exclude_patterns` argument throughoutTaylor Blau, Jun 20, 2023
  111. 08/16 refs/packed-backend.c: refactor `find_reference_location()`Taylor Blau, Jun 20, 2023
  112. 09/16 refs/packed-backend.c: implement jump lists to avoid excluded pattern(s)Taylor Blau, Jun 20, 2023
  113. Jeff KingJul 3, 2023
  114. Taylor BlauJul 3, 2023
  115. 10/16 refs/packed-backend.c: add trace2 counters for jump listTaylor Blau, Jun 20, 2023
  116. 11/16 revision.h: store hidden refs in a `strvec`Taylor Blau, Jun 20, 2023
  117. Jeff KingJul 3, 2023
  118. 12/16 refs/packed-backend.c: ignore complicated hidden refs rulesTaylor Blau, Jun 20, 2023
  119. Jeff KingJul 3, 2023
  120. Taylor BlauJul 4, 2023
  121. 13/16 refs.h: let `for_each_namespaced_ref()` take excluded patternsTaylor Blau, Jun 20, 2023
  122. 14/16 builtin/receive-pack.c: avoid enumerating hidden referencesTaylor Blau, Jun 20, 2023
  123. 15/16 upload-pack.c: avoid enumerating hidden refs where possibleTaylor Blau, Jun 20, 2023
  124. Jeff KingJul 3, 2023
  125. Taylor BlauJul 4, 2023
  126. 16/16 ls-refs.c: avoid enumerating hidden refs where possibleTaylor Blau, Jun 20, 2023
  127. Jeff KingJul 3, 2023
  128. Jeff KingJul 3, 2023
  129. 00/16 refs: implement jump lists for packed backendTaylor Blau, Jul 10, 2023
  130. 01/16 refs.c: rename `ref_filter`Taylor Blau, Jul 10, 2023
  131. 02/16 ref-filter.h: provide `REF_FILTER_INIT`Taylor Blau, Jul 10, 2023
  132. 03/16 ref-filter: clear reachable list pointers after freeingTaylor Blau, Jul 10, 2023
  133. 04/16 ref-filter: add `ref_filter_clear()`Taylor Blau, Jul 10, 2023
  134. 05/16 ref-filter.c: parameterize match functions over patternsTaylor Blau, Jul 10, 2023
  135. 06/16 builtin/for-each-ref.c: add `--exclude` optionTaylor Blau, Jul 10, 2023
  136. 07/16 refs: plumb `exclude_patterns` argument throughoutTaylor Blau, Jul 10, 2023
  137. 08/16 refs/packed-backend.c: refactor `find_reference_location()`Taylor Blau, Jul 10, 2023
  138. 09/16 refs/packed-backend.c: implement jump lists to avoid excluded pattern(s)Taylor Blau, Jul 10, 2023
  139. 10/16 refs/packed-backend.c: add trace2 counters for jump listTaylor Blau, Jul 10, 2023
  140. 11/16 revision.h: store hidden refs in a `strvec`Taylor Blau, Jul 10, 2023
  141. 12/16 refs.h: let `for_each_namespaced_ref()` take excluded patternsTaylor Blau, Jul 10, 2023
  142. 14/16 builtin/receive-pack.c: avoid enumerating hidden referencesTaylor Blau, Jul 10, 2023
  143. 13/16 refs.h: implement `hidden_refs_to_excludes()`Taylor Blau, Jul 10, 2023
  144. 15/16 upload-pack.c: avoid enumerating hidden refs where possibleTaylor Blau, Jul 10, 2023
  145. 16/16 ls-refs.c: avoid enumerating hidden refs where possibleTaylor Blau, Jul 10, 2023
  146. Junio C HamanoJul 10, 2023
  147. Patrick SteinhardtJul 11, 2023
  148. Junio C HamanoJul 11, 2023
  149. Taylor BlauJul 11, 2023

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.