Fix use of 'cache_bottom' in sparse index
cover Fix use of 'cache_bottom' in sparse index
An issue was found by @stolee after experimenting with the structure of the test repo used in 't/t1092-sparse-checkout-compatibility.sh' (patch [1/3]) and finding that certain commands (namely those that internally performed a "cache diff") failed when a sparse directory appeared lexicographically before the in-cone directory. The root cause was traced to the 'unpack_trees_options.cache_bottom' variable, which was not being advanced properly for sparse directories. The result was entries being double-unpacked or improperly unpacked by 'unpack_trees', causing failures in tests using 'git reset -- <pathspec>' and 'git diff --staged -- <pathspec>' using pathspecs.
The 'cache_bottom' was handled differently in sparse indexes in the first place based on the reasoning laid out in 17a1bb570b (unpack-trees: preserve cache_bottom, 2021-07-14). In short, the 'cache_bottom' advancement in 'mark_ce_used(...)' was disabled for sparse directories because 'unpack_callback(...)' would advance the 'cache_bottom' based on the number of index entries matching or inside the tree being unpacked. If the tree was a sparse directory, 'cache_bottom' would appropriately advance by 1, and therefore didn't need the extra advancement in 'mark_ce_used(...)'. However, this was insufficient to properly advance the 'cache_bottom' for two reasons:
1. 'unpack_callback(...)' would only advance the 'cache_bottom' for sparse directories if the operation in progress was a "cache diff" (true for 'git diff --staged' and 'git reset --mixed', but not - for instance - 'git reset --hard' or 'git read-tree -m').
2. If sparse directories were unpacked with 'unpack_index_entry(...)' (e.g., in 'git reset -- <pathspec>'), the cache tree-based advancement of 'cache_bottom' would not happen.
Luckily, the first did not appear to have any behavioral impact. However, the latter led to incorrect values being returned by 'next_cache_entry(...)' depending on the structure of the index, causing the test failures observed in 't1092'.
To fix this, the 'cache_bottom' advancement is reinstated in 'mark_ce_used(...)', and instead it is disabled in 'unpack_callback(...)' if the tree in question is a sparse directory. This corrects both the non-"cache diff" cases and the 'unpack_index_entry(...)' cases while preventing the double-advancement 17a1bb570b originally intended to avoid (patch [2/3]).
Finally, now that the cache bottom is advanced properly, we can revert the "performance improvement" introduced in f2a454e0a5 (unpack-trees: improve performance of next_cache_entry, 2021-11-29) that mitigated performance issues arising in 'next_cache_entry(...)' from the non-advancing 'cache_bottom' (patch [3/3]). The performance results in 'p2000-sparse-operations.sh' showed expected variability around 0% change in execution time (+/= 0.04s, depending on the command), with example results for potentially-affected commands below.
'git reset' master this_series
------------------------------------------------------------------------
full-v3 0.51(0.21+0.27) 0.50(0.21+0.25) -2.0% full-v4 0.51(0.22+0.27) 0.50(0.21+0.24) -2.0% sparse-v3 0.30(0.04+0.55) 0.28(0.04+0.50) -6.7% sparse-v4 0.31(0.04+0.51) 0.29(0.04+0.51) -6.5%
'git reset -- does-not-exist' master this_series
------------------------------------------------------------------------
full-v3 0.54(0.23+0.27) 0.55(0.22+0.28) +1.9% full-v4 0.56(0.25+0.26) 0.54(0.24+0.26) -3.6% sparse-v3 0.31(0.04+0.54) 0.31(0.04+0.50) +0.0% sparse-v4 0.31(0.04+0.52) 0.31(0.04+0.50) +0.0%
'git diff --cached' master this_series
-------------------------------------------------------------------------
full-v3 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% full-v4 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% sparse-v3 0.05(0.01+0.02) 0.05(0.01+0.03) +0.0% sparse-v4 0.04(0.01+0.02) 0.04(0.01+0.02) +0.0%
Thanks! -Victoria
Victoria Dye (3):
t1092: add sparse directory before cone in test repo
unpack-trees: increment cache_bottom for sparse directories Revert "unpack-trees: improve performance of next_cache_entry"
t/t1092-sparse-checkout-compatibility.sh | 6 +++- unpack-trees.c | 39 +++++++++--------------- 2 files changed, 19 insertions(+), 26 deletions(-)
base-commit: 1a4874565fa3b6668042216189551b98b4dc0b1b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1179%2Fvdye%2Fbugfix%2Fsparse-index-cache-bottom-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1179/vdye/bugfix/sparse-index-cache-bottom-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1179
Signature · 1 line
gitgitgadget
PATCH 1/3 t1092: add sparse directory before cone in test repo
From: Victoria Dye <[email protected]>
Add a sparse directory 'before/' containing files 'a' and 'b' to the test repo used in 't/t1092-sparse-checkout-compatibility.sh'. This is meant to ensure that no sparse index integrations rely on the in-cone path(s) being lexicographically first in the repo.
Unfortunately, some existing tests do not handle this repo architecture properly:
* 'add outside sparse cone'
* 'status/add: outside sparse cone'
* 'reset with pathspecs inside sparse definition'
All three of these are due to the incorrect handling of the 'unpack_trees_options.cache_bottom' when performing a cache diff via 'unpack_trees'. This will be corrected in a future patch; in the meantime, mark the tests with 'test_expect_failure'.
Finally, update the 'ls-files' test to include the 'before/' directory in its expected results.
--- t/t1092-sparse-checkout-compatibility.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-)
t/t1092-sparse-checkout-compatibility.sh +8 -4
@@ -16,7 +16,9 @@ test_expect_success 'setup' ' echo "after deep" >e && echo "after folder1" >g && echo "after x" >z && - mkdir folder1 folder2 deep x && + mkdir folder1 folder2 deep before x && + echo "before deep" >before/a && + echo "before deep again" >before/b && mkdir deep/deeper1 deep/deeper2 deep/before deep/later && mkdir deep/deeper1/deepest && mkdir deep/deeper1/deepest2 &&
@@ -312,7 +314,7 @@ test_expect_success 'deep changes during checkout' ' test_all_match git checkout base ' -test_expect_success 'add outside sparse cone' ' +test_expect_failure 'add outside sparse cone' ' init_repos && run_on_sparse mkdir folder1 &&
@@ -354,7 +356,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -565,7 +567,7 @@ test_expect_success 'checkout and reset (keep)' ' test_all_match test_must_fail git reset --keep deepest ' -test_expect_success 'reset with pathspecs inside sparse definition' ' +test_expect_failure 'reset with pathspecs inside sparse definition' ' init_repos && write_script edit-contents <<-\EOF &&
@@ -1311,6 +1313,7 @@ test_expect_success 'ls-files' ' cat >expect <<-\EOF && a + before/ deep/ e folder1-
@@ -1358,6 +1361,7 @@ test_expect_success 'ls-files' ' cat >expect <<-\EOF && a + before/ deep/ e folder1-
Signature · 1 line
gitgitgadget
PATCH 2/3 unpack-trees: increment cache_bottom for sparse directories
In reply to Victoria Dye via GitGitGadgetFrom: Victoria Dye <[email protected]>
Correct tracking of the 'cache_bottom' for cases where sparse directories are present in the index.
BACKGROUND
----------
The 'unpack_trees_options.cache_bottom' is a variable that tracks the in-progress "bottom" of the cache as 'unpack_trees()' iterates through the contents of the index. Most importantly, this value informs the sequential return values of 'next_cache_entry()' which, in the "diff cache" usage of 'unpack_callback()', are either unpacked as-is or are passed into the diff machinery.
The 'cache_bottom' is intended to track the position of the first entry in the index that has not yet been diffed or unpacked. It is advanced in two main ways: either it is incremented when an index entry is marked as "used" (in 'mark_ce_used()'), indicating that it was unpacked or diffed, or when a directory is unpacked, in which case it is increased by an amount equaling the number of index entries inside that tree.
In 17a1bb570b (unpack-trees: preserve cache_bottom, 2021-07-14), it was identified that sparse directories posed a problem to the above 'cache_bottom' advancement logic - because a sparse directory was both an index entry that could be "used" and a directory that can be unpacked, the 'cache_bottom' would be incremented too many times. To solve this problem, the 'mark_ce_used()' advancement of 'cache_bottom' was skipped for sparse directories.
INCORRECT CACHE_BOTTOM TRACKING
-------------------------------
Skipping the 'cache_bottom' advancement for sparse directories in 'mark_ce_used()' breaks down in two cases:
1. When the 'unpack_trees()' operation is *not* a "cache diff" (because the directory contents-based incrementing of 'cache_bottom' does not happen).
2. When a cache diff is performed with a pathspec (because 'unpack_index_entry()' will unpack a sparse directory not matched by the pathspec without performing the directory contents-based increment).
The former luckily does not appear to affect 'git' behavior, likely because 'cache_bottom' is largely unused (non-"cache diff" 'unpack_trees()' uses 'find_index_entry()' - rather than 'next_cache_entry()' - to find the index entries to unpack).
The latter, however, causes 'cache_bottom' to "lag behind" its intended position by an amount equal to the number of sparse directories unpacked so far with 'unpack_index_entry()'. If a repository is structured such that any sparse directories are ordered lexicographically *after* any pathspec-matching directories, though, this issue won't present any adverse behavior.
This was the case with the 't1092-sparse-checkout-compatibility.sh' tests before the addition of the 'before/' sparse directory (ordered *before* the in-cone 'deep/' directory), therefore sidestepping the issue. Once the 'before/' directory was added, though, 'cache_bottom' began to lag behind its intended position, causing 'next_cache_entry()' to return index entries it had already processed and, ultimately, an incorrect diff.
CORRECTING CACHE_BOTTOM
-----------------------
The problems observed in 't1092' come from 'cache_bottom' lagging behind in cases where the cache tree-based advancement doesn't occur. To solve this, then, the fix in 17a1bb570b is "reversed"; rather than skipping 'cache_bottom' advancement in 'mark_ce_used()', we skip the directory contents-based advancement for sparse directories. Now, every index entry can be accounted for in 'cache_bottom':
* if you're working with a single index entry, 'cache_bottom' is incremented in 'mark_ce_used()'
* if you're working with a directory that contains index entries (but is not one itself), 'cache_bottom' is incremented by the number of entries in that directory.
Finally, change the 'test_expect_failure' tests in 't1092' failing due to this bug back to 'test_expect_success'.
--- t/t1092-sparse-checkout-compatibility.sh | 6 +++--- unpack-trees.c | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-)
t/t1092-sparse-checkout-compatibility.sh +3 -3
@@ -314,7 +314,7 @@ test_expect_success 'deep changes during checkout' ' test_all_match git checkout base ' -test_expect_failure 'add outside sparse cone' ' +test_expect_success 'add outside sparse cone' ' init_repos && run_on_sparse mkdir folder1 &&
@@ -356,7 +356,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -567,7 +567,7 @@ test_expect_success 'checkout and reset (keep)' ' test_all_match test_must_fail git reset --keep deepest ' -test_expect_failure 'reset with pathspecs inside sparse definition' ' +test_expect_success 'reset with pathspecs inside sparse definition' ' init_repos && write_script edit-contents <<-\EOF &&
unpack-trees.c +8 -8
@@ -595,13 +595,6 @@ static void mark_ce_used(struct cache_entry *ce, struct unpack_trees_options *o) { ce->ce_flags |=CE_UNPACKED ; -/* -* If this is a sparse directory, don't advance cache_bottom. -* That will be advanced later using the cache-tree data. -*/ -if (S_ISSPARSEDIR (ce->ce_mode)) -return ; -if (o->cache_bottom < o->src_index->cache_nr && o->src_index->cache[o->cache_bottom] == ce) {int bottom = o->cache_bottom;
@@ -1442,7 +1435,14 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str* it does not do any look-ahead, so this is safe. */ if (matches) { - o->cache_bottom += matches; +/* +* Only increment the cache_bottom if the +* directory isn't a sparse directory index +* entry (if it is, it was already incremented) +* in 'mark_ce_used()' +*/ +if (!src[0 ] || !S_ISSPARSEDIR (src[0 ]->ce_mode)) + o->cache_bottom += matches;return mask; } }
Signature · 1 line
gitgitgadget
PATCH 3/3 Revert "unpack-trees: improve performance of next_cache_entry"
In reply to Victoria Dye via GitGitGadgetFrom: Victoria Dye <[email protected]>
This reverts commit f2a454e0a5 (unpack-trees: improve performance of next_cache_entry, 2021-11-29).
The "hint" value was originally needed to improve performance in 'git reset -- <pathspec>' caused by 'cache_bottom' lagging behind its correct value when using a sparse index. The 'cache_bottom' tracking has since been corrected, removing the need for an additional "pseudo-cache_bottom" tracking variable.
--- unpack-trees.c | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-)
unpack-trees.c +6 -17
@@ -644,24 +644,17 @@ static void mark_ce_used_same_name(struct cache_entry *ce, } } -static struct cache_entry *next_cache_entry (struct unpack_trees_options *o,int *hint) +static struct cache_entry *next_cache_entry (struct unpack_trees_options *o) {const struct index_state *index = o->src_index;int pos = o->cache_bottom; -if (*hint > pos) - pos = *hint; -while (pos < index->cache_nr) {struct cache_entry *ce = index->cache[pos]; -if (!(ce->ce_flags &CE_UNPACKED )) { - *hint = pos +1 ; +if (!(ce->ce_flags &CE_UNPACKED ))return ce; - } pos++; } - - *hint = pos;return NULL ; }
@@ -1373,13 +1366,12 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str/* Are we supposed to look at the index too? */ if (o->merge) { -int hint =-1 ;while (1 ) {int cmp;struct cache_entry *ce;if (o->diff_index_cached) - ce =next_cache_entry (o, &hint); + ce =next_cache_entry (o);else ce =find_cache_entry (info, p);
@@ -1706,7 +1698,7 @@ static int verify_absent(const struct cache_entry *,int unpack_trees (unsigned len,struct tree_desc *t,struct unpack_trees_options *o) {struct repository *repo = the_repository; -int i, hint, ret; +int i, ret;static struct cache_entry *dfc;struct pattern_list pl;int free_pattern_list =0 ;
@@ -1795,15 +1787,13 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options info.pathspec = o->pathspec;if (o->prefix) { - hint =-1 ; -/* * Unpack existing index entries that sort before the * prefix the tree is spliced into. Note that o->merge * is always true in this case. */ while (1 ) { -struct cache_entry *ce =next_cache_entry (o, &hint); +struct cache_entry *ce =next_cache_entry (o);if (!ce)break ;if (ce_in_traverse_path (ce, &info))
@@ -1824,9 +1814,8 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options/* Any left-over entries in the index? */ if (o->merge) { - hint =-1 ;while (1 ) { -struct cache_entry *ce =next_cache_entry (o, &hint); +struct cache_entry *ce =next_cache_entry (o);if (!ce)break ;if (unpack_index_entry (ce, o) <0 )
Signature · 1 line
gitgitgadget
Quoted 7 lines · Victoria Dye via GitGitGadget
...
To fix this, the 'cache_bottom' advancement is reinstated in 'mark_ce_used(...)', and instead it is disabled in 'unpack_callback(...)' if the tree in question is a sparse directory. This corrects both the non-"cache diff" cases and the 'unpack_index_entry(...)' cases while preventing the double-advancement17a1bb570boriginally intended to avoid (patch [2/3]).
;-).
v2 cover Fix use of 'cache_bottom' in sparse index
In reply to Victoria Dye via GitGitGadgetAn issue was found by @stolee after experimenting with the structure of the test repo used in 't/t1092-sparse-checkout-compatibility.sh' (patch [1/3]) and finding that certain commands (namely those that internally performed a "cache diff") failed when a sparse directory appeared lexicographically before the in-cone directory. The root cause was traced to the 'unpack_trees_options.cache_bottom' variable, which was not being advanced properly for sparse directories. The result was entries being double-unpacked or improperly unpacked by 'unpack_trees', causing failures in tests using 'git reset -- <pathspec>' and 'git diff --staged -- <pathspec>' using pathspecs.
The 'cache_bottom' was handled differently in sparse indexes in the first place based on the reasoning laid out in 17a1bb570b (unpack-trees: preserve cache_bottom, 2021-07-14). In short, the 'cache_bottom' advancement in 'mark_ce_used(...)' was disabled for sparse directories because 'unpack_callback(...)' would advance the 'cache_bottom' based on the number of index entries matching or inside the tree being unpacked. If the tree was a sparse directory, 'cache_bottom' would appropriately advance by 1, and therefore didn't need the extra advancement in 'mark_ce_used(...)'. However, this was insufficient to properly advance the 'cache_bottom' for two reasons:
1. 'unpack_callback(...)' would only advance the 'cache_bottom' for sparse directories if the operation in progress was a "cache diff" (true for 'git diff --staged' and 'git reset --mixed', but not - for instance - 'git reset --hard' or 'git read-tree -m').
2. If sparse directories were unpacked with 'unpack_index_entry(...)' (e.g., in 'git reset -- <pathspec>'), the cache tree-based advancement of 'cache_bottom' would not happen.
Luckily, the first did not appear to have any behavioral impact. However, the latter led to incorrect values being returned by 'next_cache_entry(...)' depending on the structure of the index, causing the test failures observed in 't1092'.
To fix this, the 'cache_bottom' advancement is reinstated in 'mark_ce_used(...)', and instead it is disabled in 'unpack_callback(...)' if the tree in question is a sparse directory. This corrects both the non-"cache diff" cases and the 'unpack_index_entry(...)' cases while preventing the double-advancement 17a1bb570b originally intended to avoid (patch [2/3]).
Finally, now that the cache bottom is advanced properly, we can revert the "performance improvement" introduced in f2a454e0a5 (unpack-trees: improve performance of next_cache_entry, 2021-11-29) that mitigated performance issues arising in 'next_cache_entry(...)' from the non-advancing 'cache_bottom' (patch [3/3]). The performance results in 'p2000-sparse-operations.sh' showed expected variability around 0% change in execution time (+/= 0.04s, depending on the command), with example results for potentially-affected commands below.
'git reset' master this_series
------------------------------------------------------------------------
full-v3 0.51(0.21+0.27) 0.50(0.21+0.25) -2.0% full-v4 0.51(0.22+0.27) 0.50(0.21+0.24) -2.0% sparse-v3 0.30(0.04+0.55) 0.28(0.04+0.50) -6.7% sparse-v4 0.31(0.04+0.51) 0.29(0.04+0.51) -6.5%
'git reset -- does-not-exist' master this_series
------------------------------------------------------------------------
full-v3 0.54(0.23+0.27) 0.55(0.22+0.28) +1.9% full-v4 0.56(0.25+0.26) 0.54(0.24+0.26) -3.6% sparse-v3 0.31(0.04+0.54) 0.31(0.04+0.50) +0.0% sparse-v4 0.31(0.04+0.52) 0.31(0.04+0.50) +0.0%
'git diff --cached' master this_series
-------------------------------------------------------------------------
full-v3 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% full-v4 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% sparse-v3 0.05(0.01+0.02) 0.05(0.01+0.03) +0.0% sparse-v4 0.04(0.01+0.02) 0.04(0.01+0.02) +0.0%
Changes since V1
================
* Rebase on top of 'master' (to take changes from 'vd/sparse-read-tree')
* Add 'before/' to expected index results in 't1092' test 'root directory cannot be sparse'
Thanks! -Victoria
Victoria Dye (3):
t1092: add sparse directory before cone in test repo
unpack-trees: increment cache_bottom for sparse directories Revert "unpack-trees: improve performance of next_cache_entry"
t/t1092-sparse-checkout-compatibility.sh | 7 ++++- unpack-trees.c | 39 +++++++++--------------- 2 files changed, 20 insertions(+), 26 deletions(-)
base-commit: 74cc1aa55f30ed76424a0e7226ab519aa6265061
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1179%2Fvdye%2Fbugfix%2Fsparse-index-cache-bottom-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1179/vdye/bugfix/sparse-index-cache-bottom-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1179
Range-diff vs v1:
1: 726b947bcbf ! 1: d1491d299a9 t1092: add sparse directory before cone in test repo
@@ Commit message
'unpack_trees'. This will be corrected in a future patch; in the meantime, mark the tests with 'test_expect_failure'.
- Finally, update the 'ls-files' test to include the 'before/' directory in
- its expected results.
+ Finally, update the 'ls-files' and 'root directory cannot be sparse' tests
+ to include the 'before/' directory in their expected index contents.
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'setup' '
mkdir deep/deeper1 deep/deeper2 deep/before deep/later &&
mkdir deep/deeper1/deepest &&
mkdir deep/deeper1/deepest2 &&
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'root directory cannot be sparse' ' +
+ # Verify sparse directories still present, root directory is not sparse
+ cat >expect <<-EOF &&
++ before/
+ folder1/
+ folder2/
+ x/
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'deep changes during checkout' '
test_all_match git checkout base
'
2: 8ebfebcc347 = 2: cf8dc50c300 unpack-trees: increment cache_bottom for sparse directories 3: ec6b9686e8f = 3: d74b304f560 Revert "unpack-trees: improve performance of next_cache_entry"
Signature · 1 line
gitgitgadget
PATCH v2 1/3 t1092: add sparse directory before cone in test repo
From: Victoria Dye <[email protected]>
Add a sparse directory 'before/' containing files 'a' and 'b' to the test repo used in 't/t1092-sparse-checkout-compatibility.sh'. This is meant to ensure that no sparse index integrations rely on the in-cone path(s) being lexicographically first in the repo.
Unfortunately, some existing tests do not handle this repo architecture properly:
* 'add outside sparse cone'
* 'status/add: outside sparse cone'
* 'reset with pathspecs inside sparse definition'
All three of these are due to the incorrect handling of the 'unpack_trees_options.cache_bottom' when performing a cache diff via 'unpack_trees'. This will be corrected in a future patch; in the meantime, mark the tests with 'test_expect_failure'.
Finally, update the 'ls-files' and 'root directory cannot be sparse' tests to include the 'before/' directory in their expected index contents.
--- t/t1092-sparse-checkout-compatibility.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-)
t/t1092-sparse-checkout-compatibility.sh +9 -4
@@ -16,7 +16,9 @@ test_expect_success 'setup' ' echo "after deep" >e && echo "after folder1" >g && echo "after x" >z && - mkdir folder1 folder2 deep x && + mkdir folder1 folder2 deep before x && + echo "before deep" >before/a && + echo "before deep again" >before/b && mkdir deep/deeper1 deep/deeper2 deep/before deep/later && mkdir deep/deeper1/deepest && mkdir deep/deeper1/deepest2 &&
@@ -254,6 +256,7 @@ test_expect_success 'root directory cannot be sparse' ' # Verify sparse directories still present, root directory is not sparse cat >expect <<-EOF && + before/ folder1/ folder2/ x/
@@ -337,7 +340,7 @@ test_expect_success 'deep changes during checkout' ' test_all_match git checkout base ' -test_expect_success 'add outside sparse cone' ' +test_expect_failure 'add outside sparse cone' ' init_repos && run_on_sparse mkdir folder1 &&
@@ -379,7 +382,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -590,7 +593,7 @@ test_expect_success 'checkout and reset (keep)' ' test_all_match test_must_fail git reset --keep deepest ' -test_expect_success 'reset with pathspecs inside sparse definition' ' +test_expect_failure 'reset with pathspecs inside sparse definition' ' init_repos && write_script edit-contents <<-\EOF &&
@@ -1444,6 +1447,7 @@ test_expect_success 'ls-files' ' cat >expect <<-\EOF && a + before/ deep/ e folder1-
@@ -1491,6 +1495,7 @@ test_expect_success 'ls-files' ' cat >expect <<-\EOF && a + before/ deep/ e folder1-
Signature · 1 line
gitgitgadget
PATCH v2 2/3 unpack-trees: increment cache_bottom for sparse directories
In reply to Victoria Dye via GitGitGadgetFrom: Victoria Dye <[email protected]>
Correct tracking of the 'cache_bottom' for cases where sparse directories are present in the index.
BACKGROUND
----------
The 'unpack_trees_options.cache_bottom' is a variable that tracks the in-progress "bottom" of the cache as 'unpack_trees()' iterates through the contents of the index. Most importantly, this value informs the sequential return values of 'next_cache_entry()' which, in the "diff cache" usage of 'unpack_callback()', are either unpacked as-is or are passed into the diff machinery.
The 'cache_bottom' is intended to track the position of the first entry in the index that has not yet been diffed or unpacked. It is advanced in two main ways: either it is incremented when an index entry is marked as "used" (in 'mark_ce_used()'), indicating that it was unpacked or diffed, or when a directory is unpacked, in which case it is increased by an amount equaling the number of index entries inside that tree.
In 17a1bb570b (unpack-trees: preserve cache_bottom, 2021-07-14), it was identified that sparse directories posed a problem to the above 'cache_bottom' advancement logic - because a sparse directory was both an index entry that could be "used" and a directory that can be unpacked, the 'cache_bottom' would be incremented too many times. To solve this problem, the 'mark_ce_used()' advancement of 'cache_bottom' was skipped for sparse directories.
INCORRECT CACHE_BOTTOM TRACKING
-------------------------------
Skipping the 'cache_bottom' advancement for sparse directories in 'mark_ce_used()' breaks down in two cases:
1. When the 'unpack_trees()' operation is *not* a "cache diff" (because the directory contents-based incrementing of 'cache_bottom' does not happen).
2. When a cache diff is performed with a pathspec (because 'unpack_index_entry()' will unpack a sparse directory not matched by the pathspec without performing the directory contents-based increment).
The former luckily does not appear to affect 'git' behavior, likely because 'cache_bottom' is largely unused (non-"cache diff" 'unpack_trees()' uses 'find_index_entry()' - rather than 'next_cache_entry()' - to find the index entries to unpack).
The latter, however, causes 'cache_bottom' to "lag behind" its intended position by an amount equal to the number of sparse directories unpacked so far with 'unpack_index_entry()'. If a repository is structured such that any sparse directories are ordered lexicographically *after* any pathspec-matching directories, though, this issue won't present any adverse behavior.
This was the case with the 't1092-sparse-checkout-compatibility.sh' tests before the addition of the 'before/' sparse directory (ordered *before* the in-cone 'deep/' directory), therefore sidestepping the issue. Once the 'before/' directory was added, though, 'cache_bottom' began to lag behind its intended position, causing 'next_cache_entry()' to return index entries it had already processed and, ultimately, an incorrect diff.
CORRECTING CACHE_BOTTOM
-----------------------
The problems observed in 't1092' come from 'cache_bottom' lagging behind in cases where the cache tree-based advancement doesn't occur. To solve this, then, the fix in 17a1bb570b is "reversed"; rather than skipping 'cache_bottom' advancement in 'mark_ce_used()', we skip the directory contents-based advancement for sparse directories. Now, every index entry can be accounted for in 'cache_bottom':
* if you're working with a single index entry, 'cache_bottom' is incremented in 'mark_ce_used()'
* if you're working with a directory that contains index entries (but is not one itself), 'cache_bottom' is incremented by the number of entries in that directory.
Finally, change the 'test_expect_failure' tests in 't1092' failing due to this bug back to 'test_expect_success'.
--- t/t1092-sparse-checkout-compatibility.sh | 6 +++--- unpack-trees.c | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-)
t/t1092-sparse-checkout-compatibility.sh +3 -3
@@ -340,7 +340,7 @@ test_expect_success 'deep changes during checkout' ' test_all_match git checkout base ' -test_expect_failure 'add outside sparse cone' ' +test_expect_success 'add outside sparse cone' ' init_repos && run_on_sparse mkdir folder1 &&
@@ -382,7 +382,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -593,7 +593,7 @@ test_expect_success 'checkout and reset (keep)' ' test_all_match test_must_fail git reset --keep deepest ' -test_expect_failure 'reset with pathspecs inside sparse definition' ' +test_expect_success 'reset with pathspecs inside sparse definition' ' init_repos && write_script edit-contents <<-\EOF &&
unpack-trees.c +8 -8
@@ -595,13 +595,6 @@ static void mark_ce_used(struct cache_entry *ce, struct unpack_trees_options *o) { ce->ce_flags |=CE_UNPACKED ; -/* -* If this is a sparse directory, don't advance cache_bottom. -* That will be advanced later using the cache-tree data. -*/ -if (S_ISSPARSEDIR (ce->ce_mode)) -return ; -if (o->cache_bottom < o->src_index->cache_nr && o->src_index->cache[o->cache_bottom] == ce) {int bottom = o->cache_bottom;
@@ -1478,7 +1471,14 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str* it does not do any look-ahead, so this is safe. */ if (matches) { - o->cache_bottom += matches; +/* +* Only increment the cache_bottom if the +* directory isn't a sparse directory index +* entry (if it is, it was already incremented) +* in 'mark_ce_used()' +*/ +if (!src[0 ] || !S_ISSPARSEDIR (src[0 ]->ce_mode)) + o->cache_bottom += matches;return mask; } }
Signature · 1 line
gitgitgadget
PATCH v2 3/3 Revert "unpack-trees: improve performance of next_cache_entry"
In reply to Victoria Dye via GitGitGadgetFrom: Victoria Dye <[email protected]>
This reverts commit f2a454e0a5 (unpack-trees: improve performance of next_cache_entry, 2021-11-29).
The "hint" value was originally needed to improve performance in 'git reset -- <pathspec>' caused by 'cache_bottom' lagging behind its correct value when using a sparse index. The 'cache_bottom' tracking has since been corrected, removing the need for an additional "pseudo-cache_bottom" tracking variable.
--- unpack-trees.c | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-)
unpack-trees.c +6 -17
@@ -644,24 +644,17 @@ static void mark_ce_used_same_name(struct cache_entry *ce, } } -static struct cache_entry *next_cache_entry (struct unpack_trees_options *o,int *hint) +static struct cache_entry *next_cache_entry (struct unpack_trees_options *o) {const struct index_state *index = o->src_index;int pos = o->cache_bottom; -if (*hint > pos) - pos = *hint; -while (pos < index->cache_nr) {struct cache_entry *ce = index->cache[pos]; -if (!(ce->ce_flags &CE_UNPACKED )) { - *hint = pos +1 ; +if (!(ce->ce_flags &CE_UNPACKED ))return ce; - } pos++; } - - *hint = pos;return NULL ; }
@@ -1409,13 +1402,12 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str/* Are we supposed to look at the index too? */ if (o->merge) { -int hint =-1 ;while (1 ) {int cmp;struct cache_entry *ce;if (o->diff_index_cached) - ce =next_cache_entry (o, &hint); + ce =next_cache_entry (o);else ce =find_cache_entry (info, p);
@@ -1777,7 +1769,7 @@ static int verify_absent(const struct cache_entry *,int unpack_trees (unsigned len,struct tree_desc *t,struct unpack_trees_options *o) {struct repository *repo = the_repository; -int i, hint, ret; +int i, ret;static struct cache_entry *dfc;struct pattern_list pl;int free_pattern_list =0 ;
@@ -1869,15 +1861,13 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options info.pathspec = o->pathspec;if (o->prefix) { - hint =-1 ; -/* * Unpack existing index entries that sort before the * prefix the tree is spliced into. Note that o->merge * is always true in this case. */ while (1 ) { -struct cache_entry *ce =next_cache_entry (o, &hint); +struct cache_entry *ce =next_cache_entry (o);if (!ce)break ;if (ce_in_traverse_path (ce, &info))
@@ -1898,9 +1888,8 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options/* Any left-over entries in the index? */ if (o->merge) { - hint =-1 ;while (1 ) { -struct cache_entry *ce =next_cache_entry (o, &hint); +struct cache_entry *ce =next_cache_entry (o);if (!ce)break ;if (unpack_index_entry (ce, o) <0 )
Signature · 1 line
gitgitgadget
Re: [PATCH v2 2/3] unpack-trees: increment cache_bottom for sparse directories
In reply to Victoria Dye via GitGitGadgetQuoted 4 lines · Victoria Dye via GitGitGadget
From: Victoria Dye <[email protected]>
Correct tracking of the '
cache_bottom' for cases where sparse directories are present in the index.
Thank you for the detailed background (that I cut from my reply).
Quoted 8 lines
CORRECTING
CACHE_BOTTOM
-----------------------
The problems observed in 't1092' come from 'cache_bottom' lagging behind in cases where the cache tree-based advancement doesn't occur. To solve this, then, the fix in17a1bb570bis "reversed"; rather than skipping 'cache_bottom' advancement in 'mark_ce_used()', we skip the directory contents-based advancement for sparse directories. Now, every index entry can be accounted for in 'cache_bottom':
I have sufficient background to be confident that you are doing the right thing here!
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'commit including unstaged changes' '
Quoted 8 lines
t/t1092-sparse-checkout-compatibility.sh +3
-3-test_expect_failure 'add outside sparse cone' ' +test_expect_success 'add outside sparse cone' ' -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' -test_expect_failure 'reset with pathspecs inside sparse definition' ' +test_expect_success 'reset with pathspecs inside sparse definition' '
Love to see it.
unpack-trees.c static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str · line 1482
Quoted 11 lines
unpack-trees.c +8
-1if (matches) { - o->cache_bottom += matches; +/* +* Only increment the cache_bottom if the +* directory isn't a sparse directory index +* entry (if it is, it was already incremented) +* in 'mark_ce_used()' +*/ +if (!src[0 ] || !S_ISSPARSEDIR (src[0 ]->ce_mode)) + o->cache_bottom += matches;return mask;
Looks great.
Thanks,
-Stolee
Quoted 6 lines · Victoria Dye via GitGitGadget
To fix this, the '
cache_bottom' advancement is reinstated in 'mark_ce_used(...)', and instead it is disabled in 'unpack_callback(...)' if the tree in question is a sparse directory. This corrects both the non-"cache diff" cases and the 'unpack_index_entry(...)' cases while preventing the double-advancement17a1bb570boriginally intended to avoid (patch [2/3]).
Thank you for digging deep and finding the root cause here _and_ the correct fix.
Quoted 23 lines
Finally, now that the cache bottom is advanced properly, we can revert the "performance improvement" introduced in
f2a454e0a5(unpack-trees: improve performance ofnext_cache_entry, 2021-11-29) that mitigated performance issues arising in 'next_cache_entry(...)' from the non-advancing 'cache_bottom' (patch [3/3]). The performance results in 'p2000-sparse-operations.sh' showed expected variability around 0% change in execution time (+/= 0.04s, depending on the command), with example results for potentially-affected commands below.'git reset' master
this_series
------------------------------------------------------------------------full-v3 0.51(0.21+0.27) 0.50(0.21+0.25) -2.0% full-v4 0.51(0.22+0.27) 0.50(0.21+0.24) -2.0% sparse-v3 0.30(0.04+0.55) 0.28(0.04+0.50) -6.7% sparse-v4 0.31(0.04+0.51) 0.29(0.04+0.51) -6.5%'git reset -- does-not-exist' master
this_series
------------------------------------------------------------------------full-v3 0.54(0.23+0.27) 0.55(0.22+0.28) +1.9% full-v4 0.56(0.25+0.26) 0.54(0.24+0.26) -3.6% sparse-v3 0.31(0.04+0.54) 0.31(0.04+0.50) +0.0% sparse-v4 0.31(0.04+0.52) 0.31(0.04+0.50) +0.0%
Quoted 6 lines
'git diff --cached' master
this_series
-------------------------------------------------------------------------full-v3 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% full-v4 0.09(0.04+0.04) 0.09(0.04+0.04) +0.0% sparse-v3 0.05(0.01+0.02) 0.05(0.01+0.03) +0.0% sparse-v4 0.04(0.01+0.02) 0.04(0.01+0.02) +0.0%
I'm glad this also makes things simpler here. It's interesting that it previously manifested only as a performance issue and not a correctness issue.
I carefully read these patches and think they are ready to go.
Thanks,
-Stolee
Re: [PATCH v2 2/3] unpack-trees: increment cache_bottom for sparse directories
In reply to Derrick StoleeQuoted 17 lines · Derrick Stolee
Quoted 4 lines · Victoria Dye via GitGitGadget
From: Victoria Dye <[email protected]>
Correct tracking of the '
cache_bottom' for cases where sparse directories are present in the index.Thank you for the detailed background (that I cut from my reply).
Quoted 8 lines
CORRECTING
CACHE_BOTTOM
-----------------------
The problems observed in 't1092' come from 'cache_bottom' lagging behind in cases where the cache tree-based advancement doesn't occur. To solve this, then, the fix in17a1bb570bis "reversed"; rather than skipping 'cache_bottom' advancement in 'mark_ce_used()', we skip the directory contents-based advancement for sparse directories. Now, every index entry can be accounted for in 'cache_bottom':
Quoted 2 lines
I have sufficient background to be confident that you are doing the right thing here!
;-)
The "partly lagging" walking of the index guided by cache_bottom has been a tricky thing in the unpack_trees machinery, and I am very happy that we now have two more experts on the topic ;-)
Thanks, both.