From: Erik Elfström <hidden> Date: 2016-06-15 23:04:28
I've marked this RFC since there are known problems here.
v2 of the patch can be found here:
http://thread.gmane.org/gmane.comp.version-control.git/267023/focus=267023
Changes in v3:
* Created setup.c:read_gitfile_gently to use for submodule
probing
* Cleanup of some tests by use of test_commit helper
* Added more tests of cleaning in the presence of submodules
* Reversed expectation of test for cleaning nested bare repos.
They are now expected to be cleaned. Added one more case.
* Fixed bug where submodules could be cleaned by using new
read_gitfile_gently for additional submodule check in
clean.c:is_git_repository
* Attempt to change behavior of patch implementation to clean
bare repositories (only partially successful)
* Reworded commit message of the performance fix commit
Known Problems:
* Unsure about the setup.c:read_gitfile refactor, feels a bit
messy?
* Potentially a missing sanity check of git file size in
setup.c:read_gitfile_gently_or_non_gently
* We still get a behavioral change for empty bare repositories
placed in a ".git" directory. Currently we clean empty bare
repos in a .git folder but not non-empty one. After this
patch we won't clean either. How serious is this? Is there
an easy fix (preferebly to clean all bare repositories)?
* Still have issues in the performance tests, see comments
from Thomas Gummerer on v2
Thanks to Junio C Hamano and Jeff King for spotting fundamental
problems in v2 and suggesting a solution.
Erik Elfström (4):
setup: add gentle version of read_gitfile
t7300: add tests to document behavior of clean and nested git
p7300: add performance tests for clean
clean: improve performance when removing lots of directories
builtin/clean.c | 25 ++++++++--
cache.h | 1 +
setup.c | 94 ++++++++++++++++++++++++++++---------
t/perf/p7300-clean.sh | 37 +++++++++++++++
t/t7300-clean.sh | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 257 insertions(+), 25 deletions(-)
create mode 100755 t/perf/p7300-clean.sh
--
2.4.0.rc2.5.g2871d5e
From: Erik Elfström <hidden> Date: 2016-06-15 23:04:28
read_gitfile will die on most error cases. This makes it unsuitable
for speculative calls. Extract the core logic and provide a gentle
version that returns NULL on failure.
The first usecase of the new gentle version will be to probe for
submodules during git clean.
Helped-by: Junio C Hamano [off-list ref]
Helped-by: Jeff King [off-list ref]
Signed-off-by: Erik Elfström <redacted>
---
If this is going to be used for speculative probing should there
be a sanity check before:
buf = xmalloc(st.st_size + 1);
len = read_in_full(fd, buf, st.st_size);
Something like:
if (st.st_size > PATH_MAX*2) {
error = N;
goto cleanup_return;
{
What do you think?
cache.h | 1 +
setup.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++---------------
2 files changed, 74 insertions(+), 21 deletions(-)
From: Erik Elfström <hidden> Date: 2016-06-15 23:04:28
"git clean" uses resolve_gitlink_ref() to check for the presence of
nested git repositories, but it has the drawback of creating a
ref_cache entry for every directory that should potentially be
cleaned. The linear search through the ref_cache list causes a massive
performance hit for large number of directories.
Modify clean.c:remove_dirs to use setup.c:is_git_directory and
setup.c:read_gitfile_gently instead.
Both these functions will open files and parse contents when they find
something that looks like a git repository. This is ok from a
performance standpoint since finding repository candidates should be
comparatively rare.
Using is_git_directory and read_gitfile_gently should give a more
standardized check for what is and what isn't a git repository but
also gives a slight behavioral change. We will now detect and respect
empty nested git repositories (only init run) and empty bare
repositories that have been placed in a ".git" directory. We will also
no longer die when cleaning a file named ".git" with garbage content
(it will be cleaned instead). Update t7300 to reflect this.
The time to clean an untracked directory containing 100000 sub
directories went from 61s to 1.7s after this change.
Helped-by: Jeff King [off-list ref]
Signed-off-by: Erik Elfström <redacted>
---
builtin/clean.c | 25 +++++++++++++++++++++----
t/t7300-clean.sh | 8 +++-----
2 files changed, 24 insertions(+), 9 deletions(-)
@@ -455,7 +455,7 @@ test_expect_success 'nested git work tree' '!test-dbar'-test_expect_failure'should clean things that almost look like git but are not''+test_expect_success'should clean things that almost look like git but are not''rm-fralmost_gitalmost_bare_gitalmost_submodule&&mkdir-palmost_git/.git/objects&&mkdir-palmost_git/.git/refs&&
@@ -468,8 +468,6 @@ test_expect_failure 'should clean things that almost look like git but are not'garbageEOFtest_when_finished"rm -rf almost_*"&&-## This will fail due to die("Invalid gitfile format: %s", path); in-## setup.c:read_gitfile.gitclean-f-d&&test_path_is_missingalmost_git&&test_path_is_missingalmost_bare_git&&
@@ -501,7 +499,7 @@ test_expect_success 'should not clean submodules' 'test_path_is_missingto_clean'-test_expect_failure'nested (empty) git should be kept''+test_expect_success'nested (empty) git should be kept''rm-frfoobar&&gitinitfoo&&mkdirbar&&
@@ -523,7 +521,7 @@ test_expect_success 'nested bare repositories should be cleaned' 'test_path_is_missingsubdir'-test_expect_success'nested (empty) bare repositories should be cleaned even when in .git''+test_expect_failure'nested (empty) bare repositories should be cleaned even when in .git''rm-frstrange_bare&&mkdirstrange_bare&&gitinit--barestrange_bare/.git&&
@@ -455,6 +455,133 @@ test_expect_success 'nested git work tree' '!test-dbar'+test_expect_failure'should clean things that almost look like git but are not''+rm-fralmost_gitalmost_bare_gitalmost_submodule&&+mkdir-palmost_git/.git/objects&&+mkdir-palmost_git/.git/refs&&+cat>almost_git/.git/HEAD<<-\EOF&&+garbage+EOF+cp-ralmost_git/.git/almost_bare_git&&+mkdiralmost_submodule/&&+cat>almost_submodule/.git<<-\EOF&&+garbage+EOF+test_when_finished"rm -rf almost_*"&&+## This will fail due to die("Invalid gitfile format: %s", path); in+## setup.c:read_gitfile.+gitclean-f-d&&+test_path_is_missingalmost_git&&+test_path_is_missingalmost_bare_git&&+test_path_is_missingalmost_submodule+'++test_expect_success'should not clean submodules''+rm-frrepoto_cleansub1sub2&&+mkdirrepoto_clean&&+(+cdrepo&&+gitinit&&+test_commitmsghello.world+)&&+gitsubmoduleadd./repo/.gitsub1&&+gitcommit-m"sub1"&&+gitbranchbefore_sub2&&+gitsubmoduleadd./repo/.gitsub2&&+gitcommit-m"sub2"&&+gitcheckoutbefore_sub2&&+>to_clean/should_clean.this&&+gitclean-f-d&&+test_path_is_filerepo/.git/index&&+test_path_is_filerepo/hello.world&&+test_path_is_filesub1/.git&&+test_path_is_filesub1/hello.world&&+test_path_is_filesub2/.git&&+test_path_is_filesub2/hello.world&&+test_path_is_missingto_clean+'++test_expect_failure'nested (empty) git should be kept''+rm-frfoobar&&+gitinitfoo&&+mkdirbar&&+>bar/goodbye.people&&+gitclean-f-d&&+test_path_is_filefoo/.git/HEAD&&+test_path_is_missingbar+'++test_expect_success'nested bare repositories should be cleaned''+rm-frbare1bare2subdir&&+gitinit--barebare1&&+gitclone--local--bare.bare2&&+mkdirsubdir&&+cp-rbare2subdir/bare3&&+gitclean-f-d&&+test_path_is_missingbare1&&+test_path_is_missingbare2&&+test_path_is_missingsubdir+'++test_expect_success'nested (empty) bare repositories should be cleaned even when in .git''+rm-frstrange_bare&&+mkdirstrange_bare&&+gitinit--barestrange_bare/.git&&+gitclean-f-d&&+test_path_is_missingstrange_bare+'++test_expect_failure'nested (non-empty) bare repositories should be cleaned even when in .git''+rm-frstrange_bare&&+mkdirstrange_bare&&+gitclone--local--bare.strange_bare/.git&&+gitclean-f-d&&+test_path_is_missingstrange_bare+'++test_expect_success'giving path in nested git work tree will remove it''+rm-frfoo&&+mkdirfoo&&+(+cdfoo&&+gitinit&&+mkdir-pbar/baz&&+test_commitmsgbar/baz/hello.world+)&&+gitclean-f-dfoo/bar/baz&&+test_path_is_filefoo/.git/HEAD&&+test_path_is_dirfoo/bar/&&+test_path_is_missingfoo/bar/baz+'++test_expect_success'giving path to nested .git will not remove it''+rm-frfoo&&+mkdirfoobar&&+(+cdfoo&&+gitinit&&+test_commitmsghello.world+)&&+gitclean-f-dfoo/.git&&+test_path_is_filefoo/.git/HEAD&&+test_path_is_dirfoo/.git/refs&&+test_path_is_dirfoo/.git/objects&&+test_path_is_dirbar/+'++test_expect_success'giving path to nested .git/ will remove contents''+rm-frfoobar&&+mkdirfoobar&&+(+cdfoo&&+gitinit&&+test_commitmsghello.world+)&&+gitclean-f-dfoo/.git/&&+test_path_is_dirfoo/.git&&+test_dir_is_emptyfoo/.git+'+ test_expect_success'force removal of nested git work tree''rm-frfoobarbaz&&mkdir-pfoobarbaz/boo&&
@@ -0,0 +1,37 @@+#!/bin/sh++test_description="Test git-clean performance"++../perf-lib.sh++test_perf_large_repo+test_checkout_worktree++test_expect_success'setup untracked directory with many sub dirs''+rm-rf500_sub_dirs50000_sub_dirsclean_test_dir&&+mkdir500_sub_dirs50000_sub_dirsclean_test_dir&&+foriin$(test_seq1500)+do+mkdir500_sub_dirs/dir$i||return$?+done&&+foriin$(test_seq1100)+do+cp-r500_sub_dirs50000_sub_dirs/dir$i||return$?+done+'++test_perf'clean many untracked sub dirs, check for nested git''+rm-rfclean_test_dir/50000_sub_dirs_cpy&&+cp-r50000_sub_dirsclean_test_dir/50000_sub_dirs_cpy&&+gitclean-q-f-dclean_test_dir/&&+test_dir_is_emptyclean_test_dir+'++test_perf'clean many untracked sub dirs, ignore nested git''+rm-rfclean_test_dir/50000_sub_dirs_cpy&&+cp-r50000_sub_dirsclean_test_dir/50000_sub_dirs_cpy&&+gitclean-q-f-f-dclean_test_dir/&&+test_dir_is_emptyclean_test_dir+'++test_done
From: Thomas Gummerer <hidden> Date: 2016-06-15 23:04:29
On 04/18, Erik Elfström wrote:
* Still have issues in the performance tests, see comments
from Thomas Gummerer on v2
I've looked at the "modern" style tests again, and I don't the code
churn is worth it just for using them for the performance tests. If
anyone wants to take a look at the code, it's at
github.com/tgummerer/git tg/perf-lib.
I think adding the test_perf_setup_cleanup command would make more
sense in this case. If you want I can send a patch for that.
From: erik elfström <hidden> Date: 2016-06-15 23:04:30
Ok, thanks for looking into this.
I have no well founded opinions on the implementation but I do
think the performance tests would be more meaningful if the
setup/cleanup code could be removed from the timed section.
If the community agrees on an implementation I would be happy
to convert the new tests, either directly in this series or as a follow
up if that is preferred.
/Erik
On Tue, Apr 21, 2015 at 12:14 AM, Thomas Gummerer [off-list ref] wrote:
On 04/18, Erik Elfström wrote:
quoted
* Still have issues in the performance tests, see comments
from Thomas Gummerer on v2
I've looked at the "modern" style tests again, and I don't the code
churn is worth it just for using them for the performance tests. If
anyone wants to take a look at the code, it's at
github.com/tgummerer/git tg/perf-lib.
I think adding the test_perf_setup_cleanup command would make more
sense in this case. If you want I can send a patch for that.
From: Jeff King <hidden> Date: 2016-06-15 23:04:30
On Tue, Apr 21, 2015 at 08:21:37PM +0200, erik elfström wrote:
Ok, thanks for looking into this.
I have no well founded opinions on the implementation but I do
think the performance tests would be more meaningful if the
setup/cleanup code could be removed from the timed section.
If the community agrees on an implementation I would be happy
to convert the new tests, either directly in this series or as a follow
up if that is preferred.
If I understand correctly, the reason that you need per-run setup is
that your "git clean" command actually cleans things, and you need to
restore the original state for each time-trial. Can you instead use "git
clean -n" to do a dry-run? I think what you are timing is really the
"figure out what to clean" step, and not the cleaning itself.
-Peff
From: erik elfström <hidden> Date: 2016-06-15 23:04:30
On Tue, Apr 21, 2015 at 11:24 PM, Jeff King [off-list ref] wrote:
If I understand correctly, the reason that you need per-run setup is
that your "git clean" command actually cleans things, and you need to
restore the original state for each time-trial. Can you instead use "git
clean -n" to do a dry-run? I think what you are timing is really the
"figure out what to clean" step, and not the cleaning itself.
-Peff
Yes, that is the problem. A dry run will spot this particular performance
issue but maybe we lose some value as a general performance test if
we only do "half" the clean? Admittedly we clearly lose some value in
the current state as well due to the copying taking more time than the
cleaning. I could go either way here.
/Erik
From: Jeff King <hidden> Date: 2016-06-15 23:04:30
On Wed, Apr 22, 2015 at 09:30:20PM +0200, erik elfström wrote:
On Tue, Apr 21, 2015 at 11:24 PM, Jeff King [off-list ref] wrote:
quoted
If I understand correctly, the reason that you need per-run setup is
that your "git clean" command actually cleans things, and you need to
restore the original state for each time-trial. Can you instead use "git
clean -n" to do a dry-run? I think what you are timing is really the
"figure out what to clean" step, and not the cleaning itself.
Yes, that is the problem. A dry run will spot this particular performance
issue but maybe we lose some value as a general performance test if
we only do "half" the clean? Admittedly we clearly lose some value in
the current state as well due to the copying taking more time than the
cleaning. I could go either way here.
I guess it is a matter of opinion. I think testing only the "find out
what to clean" half separately is actually beneficial, because it helps
us isolate any slowdown. If we want to add a test for the other half, we
can, but I do not actually think it is currently that interesting (it is
just calling unlink() in a loop).
So even leaving the practical matters aside, I do not think it is a bad
thing to split it up. When you add in the fact that it is practically
much easier to test the first half, it seems to me that testing just
that is a good first step.
-Peff
From: erik elfström <hidden> Date: 2016-06-15 23:04:30
On Wed, Apr 22, 2015 at 9:46 PM, Jeff King [off-list ref] wrote:
On Wed, Apr 22, 2015 at 09:30:20PM +0200, erik elfström wrote:
quoted
Yes, that is the problem. A dry run will spot this particular performance
issue but maybe we lose some value as a general performance test if
we only do "half" the clean? Admittedly we clearly lose some value in
the current state as well due to the copying taking more time than the
cleaning. I could go either way here.
I guess it is a matter of opinion. I think testing only the "find out
what to clean" half separately is actually beneficial, because it helps
us isolate any slowdown. If we want to add a test for the other half, we
can, but I do not actually think it is currently that interesting (it is
just calling unlink() in a loop).
So even leaving the practical matters aside, I do not think it is a bad
thing to split it up. When you add in the fact that it is practically
much easier to test the first half, it seems to me that testing just
that is a good first step.
-Peff
Sounds reasonable to me. I'll make this change in v4, thanks!
(Sorry for the duplicate email Jeff, I'm bad at this mailing list thing...)
/Erik