From: Jean-Jacques Lafay <redacted>
Date: Sat, 10 Nov 2012 18:36:10 +0100
In large repos, the recursion implementation of contains(commit,
commit_list) may result in a stack overflow. Replace the recursion with
a loop to fix it.
This problem is more apparent on Windows than on Linux, where the stack
is more limited by default.
See also this thread on the msysGit list:
https://groups.google.com/d/topic/msysgit/FqT6boJrb2g/discussion
[jes: re-written to imitate the original recursion more closely]
Thomas Braun pointed out several documentation shortcomings.
Signed-off-by: Jean-Jacques Lafay <redacted>
Signed-off-by: Johannes Schindelin <redacted>
Tested-by: Stepan Kasal <redacted>
Thanks-to: Thomas Braun [off-list ref]
---
builtin/tag.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++----------
t/t7004-tag.sh | 21 +++++++++++++++
2 files changed, 88 insertions(+), 14 deletions(-)
@@ -73,11 +73,13 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)return0;}-staticintcontains_recurse(structcommit*candidate,+/*+*Testwhetherthecandidateoroneofitsparentsiscontainedinthelist.+*Donotrecursetofindout,though,butreturn-1ifinconclusive.+*/+staticintcontains_test(structcommit*candidate,conststructcommit_list*want){-structcommit_list*p;-/* was it previously marked as containing a want commit? */if(candidate->object.flags&TMP_MARK)return1;
@@ -85,26 +87,77 @@ static int contains_recurse(struct commit *candidate,if(candidate->object.flags&UNINTERESTING)return0;/* or are we it? */-if(in_commit_list(want,candidate))+if(in_commit_list(want,candidate)){+candidate->object.flags|=TMP_MARK;return1;+}if(parse_commit(candidate)<0)return0;-/* 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;-return1;-}-}-candidate->object.flags|=UNINTERESTING;-return0;+return-1;+}++/*+*Mimickingtherealstack,thisstacklivesontheheap,avoidingstack+*overflows.+*+*Ateachrecursionstep,thestackitemspointstothecommitswhose+*ancestorsaretobeinspected.+*/+structstack{+intnr,alloc;+structstack_entry{+structcommit*commit;+structcommit_list*parents;+}*stack;+};++staticvoidpush_to_stack(structcommit*candidate,structstack*stack)+{+intindex=stack->nr++;+ALLOC_GROW(stack->stack,stack->nr,stack->alloc);+stack->stack[index].commit=candidate;+stack->stack[index].parents=candidate->parents;}staticintcontains(structcommit*candidate,conststructcommit_list*want){-returncontains_recurse(candidate,want);+structstackstack={0,0,NULL};+intresult=contains_test(candidate,want);++if(result>=0)+returnresult;++push_to_stack(candidate,&stack);+while(stack.nr){+structstack_entry*entry=&stack.stack[stack.nr-1];+structcommit*commit=entry->commit;+structcommit_list*parents=entry->parents;++if(!parents){+commit->object.flags=UNINTERESTING;+stack.nr--;+}+/*+*Ifwejustpoppedthestack,parents->itemhasbeenmarked,+*thereforecontains_testwillreturnameaningful0or1.+*/+elseswitch(contains_test(parents->item,want)){+case1:+commit->object.flags|=TMP_MARK;+stack.nr--;+break;+case0:+entry->parents=parents->next;+break;+default:+push_to_stack(parents->item,&stack);+break;+}+}+free(stack.stack);+returncontains_test(candidate,want);}staticvoidshow_tag_lines(constunsignedchar*sha1,intlines)
@@ -1380,4 +1380,25 @@ test_expect_success 'multiple --points-at are OR-ed together' 'test_cmpexpectactual'+>expect+# ulimit is a bash builtin; we can rely on that in MinGW, but nowhere else+test_expect_successMINGW'--contains works in a deep repo''+ulimit-s64+i=1&&+whiletest$i-lt1000+do+echo"commit refs/heads/master+committerAUThor<author@example.com>$((1000000000+$i*100))+0200+data<<EOF+commit#$i+EOF"+test$i=1&&echo"from refs/heads/master^0"+i=$(($i+1))+done|gitfast-import&&+gitcheckoutmaster&&+gittagfar-far-awayHEAD^&&+gittag--containsHEAD>actual&&+test_cmpexpectactual+'+ test_done
From: Jeff King <hidden> Date: 2016-06-15 23:00:42
On Wed, Apr 16, 2014 at 04:15:19PM +0200, Stepan Kasal wrote:
From: Jean-Jacques Lafay <redacted>
Date: Sat, 10 Nov 2012 18:36:10 +0100
In large repos, the recursion implementation of contains(commit,
commit_list) may result in a stack overflow. Replace the recursion with
a loop to fix it.
This problem is more apparent on Windows than on Linux, where the stack
is more limited by default.
I think this is a good thing to be doing, and it looks mostly good to
me. A few comments:
-static int contains_recurse(struct commit *candidate,
+/*
+ * Test whether the candidate or one of its parents is contained in the list.
+ * Do not recurse to find out, though, but return -1 if inconclusive.
+ */
+static int contains_test(struct commit *candidate,
const struct commit_list *want)
Can we turn this return value into
enum {
CONTAINS_UNKNOWN = -1,
CONTAINS_NO = 0,
CONTAINS_YES = 1,
} contains_result;
to make the code a little more self-documenting?
static int contains(struct commit *candidate, const struct commit_list *want)
{
- return contains_recurse(candidate, want);
+ struct stack stack = { 0, 0, NULL };
+ int result = contains_test(candidate, want);
+
+ if (result >= 0)
+ return result;
Then this can become:
if (result != CONTAINS_UNKNOWN)
return result;
+ /*
+ * If we just popped the stack, parents->item has been marked,
+ * therefore contains_test will return a meaningful 0 or 1.
+ */
+ else switch (contains_test(parents->item, want)) {
+ case 1:
+ commit->object.flags |= TMP_MARK;
+ stack.nr--;
+ break;
+ case 0:
+ entry->parents = parents->next;
+ break;
+ default:
+ push_to_stack(parents->item, &stack);
+ break;
+ }
And if we have an enum, this switch() becomes more readable (the
"default" here threw me off initially, because it is actually just
looking for "-1").
+>expect
+# ulimit is a bash builtin; we can rely on that in MinGW, but nowhere else
+test_expect_success MINGW '--contains works in a deep repo' '
+ ulimit -s 64
It would be nice to test this on Linux.
Can we do something like:
test_lazy_prereq BASH 'bash --version'
test_expect_success BASH '--contains works in a deep repo' '
... setup repo ...
bash -c "ulimit -s 64 && git tag --contains HEAD" >actual &&
test_cmp expect actual
'
As a bonus, then our "ulimit" call does not pollute the environment of
subsequent tests.
-Peff
From: Johannes Schindelin <hidden> Date: 2016-06-15 23:00:43
Hi Peff,
On Wed, 16 Apr 2014, Jeff King wrote:
On Wed, Apr 16, 2014 at 04:15:19PM +0200, Stepan Kasal wrote:
quoted
From: Jean-Jacques Lafay at Sat, 10 Nov 2012 18:36:10 +0100
In large repos, the recursion implementation of contains(commit,
commit_list) may result in a stack overflow. Replace the recursion
with a loop to fix it.
This problem is more apparent on Windows than on Linux, where the
stack is more limited by default.
I think this is a good thing to be doing, and it looks mostly good to
me. A few comments:
quoted
-static int contains_recurse(struct commit *candidate,
+/*
+ * Test whether the candidate or one of its parents is contained in the list.
+ * Do not recurse to find out, though, but return -1 if inconclusive.
+ */
+static int contains_test(struct commit *candidate,
const struct commit_list *want)
Can we turn this return value into
enum {
CONTAINS_UNKNOWN = -1,
CONTAINS_NO = 0,
CONTAINS_YES = 1,
} contains_result;
to make the code a little more self-documenting?
Good idea!
[... detailed instructions what changes are implied by the enum ...]
quoted
+>expect
+# ulimit is a bash builtin; we can rely on that in MinGW, but nowhere else
+test_expect_success MINGW '--contains works in a deep repo' '
+ ulimit -s 64
It would be nice to test this on Linux.
Can we do something like:
test_lazy_prereq BASH 'bash --version'
test_expect_success BASH '--contains works in a deep repo' '
... setup repo ...
bash -c "ulimit -s 64 && git tag --contains HEAD" >actual &&
test_cmp expect actual
'
As a bonus, then our "ulimit" call does not pollute the environment of
subsequent tests.
I tried running the test on my Linux box, but it doesn't fail with the
existing recursive code. So I tried a few different stack sizes, like:
for i in `seq 1 64`; do
bash -c "
ulimit -s $i &&
../../git tag --contains HEAD ||
echo fail $i"
done
The results are strangely non-deterministic, but with -O0, we generally
die reliably below about 60. With -O2, though, it's more like 43. We
can't go _too_ low here, though, as lots of things start breaking around
32.
If we instead bump the size of the history to 2000 commits, then I
reliably fail with a 64k stack (even with -O2, it needs around 80k).
Of course those numbers are all black magic, and are going to vary based
on the system, the compiler, settings, etc. My system is 64-bit, and the
current code needs at least 3 pointers per recursive invocation. So
we're spending ~46K on those variables, not counting any calling
convention overhead (and presumably we at least need a function return
pointer there). So a 32-bit system might actually get by, as it would
need half as much.
So we can bump the depth further; probably 4000 is enough for any system
to fail with a 64k stack. The deeper we make it, the longer it takes to
run the test, though. At 4000, my machine seems to take about 300ms to
run it. That's may be OK.
-Peff
I tried running the test on my Linux box, but it doesn't fail with the
existing recursive code.
I cannot recall how I came to choose 64, but I *think* I only tested on
Windows, and I *think* I reduced the number of tags in order to make
things faster (Windows is *unbearably* slow with spawn-happy programs such
as Git's tests -- literally every single line in a shell script tests the
patience of this developer, running the complete test suite with 15
parallel threads takes several hours, no kidding).
The results are strangely non-deterministic, but with -O0, we generally
die reliably below about 60. With -O2, though, it's more like 43. We
can't go _too_ low here, though, as lots of things start breaking around
32.
How about using 40, then? I am more interested in reducing the runtime
than reducing the number of false negatives. The problem will be exercised
enough on Windows, but not if the test suite becomes even slower than it
already is.
Ciao,
Johannes
From: Jeff King <hidden> Date: 2016-06-15 23:00:44
On Thu, Apr 17, 2014 at 11:52:56PM +0200, Johannes Schindelin wrote:
quoted
I tried running the test on my Linux box, but it doesn't fail with the
existing recursive code.
I cannot recall how I came to choose 64, but I *think* I only tested on
Windows, and I *think* I reduced the number of tags in order to make
things faster (Windows is *unbearably* slow with spawn-happy programs such
as Git's tests -- literally every single line in a shell script tests the
patience of this developer, running the complete test suite with 15
parallel threads takes several hours, no kidding).
Yeah, I figured speed had something to do with it. However, since you
are using a bash loop to generate the input (and it should all be done
as builtins in bash, I think), and fast-import to create the objects, I
don't think bumping it will actually increase your process count.
quoted
The results are strangely non-deterministic, but with -O0, we generally
die reliably below about 60. With -O2, though, it's more like 43. We
can't go _too_ low here, though, as lots of things start breaking around
32.
How about using 40, then? I am more interested in reducing the runtime
than reducing the number of false negatives. The problem will be exercised
enough on Windows, but not if the test suite becomes even slower than it
already is.
I'm OK with doing that. My biggest concern is that it will cause false
positives on systems that are hungrier for stack space, but we can
address that if it happens.
-Peff
From: Jean-Jacques Lafay <redacted>
In large repos, the recursion implementation of contains(commit,
commit_list) may result in a stack overflow. Replace the recursion with
a loop to fix it.
This problem is more apparent on Windows than on Linux, where the stack
is more limited by default.
See also this thread on the msysGit list:
https://groups.google.com/d/topic/msysgit/FqT6boJrb2g/discussion
[jes: re-written to imitate the original recursion more closely]
Thomas Braun pointed out several documentation shortcomings.
Tests are run only if ulimit -s is available. This means they cannot
be run on Windows.
Signed-off-by: Jean-Jacques Lafay <redacted>
Signed-off-by: Johannes Schindelin <redacted>
Tested-by: Stepan Kasal <redacted>
---
Hello,
I have found out that "ulimit -s" does not work on Windows.
Adding this as a prerequisite, we will skip the test there.
On Thu, Apr 17, 2014 at 05:32:38PM -0400, Jeff King wrote:
So we can bump the depth further; probably 4000 is enough for any system
to fail with a 64k stack. The deeper we make it, the longer it takes to
run the test, though. At 4000, my machine seems to take about 300ms to
run it. That's may be OK.
Consequently, we can accept this proposal.
Stepan Kasal
builtin/tag.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++----------
t/t7004-tag.sh | 23 +++++++++++++++
2 files changed, 98 insertions(+), 15 deletions(-)
@@ -80,11 +80,19 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)return0;}-staticintcontains_recurse(structcommit*candidate,+enumcontains_result{+CONTAINS_UNKNOWN=-1,+CONTAINS_NO=0,+CONTAINS_YES=1,+};++/*+*Testwhetherthecandidateoroneofitsparentsiscontainedinthelist.+*Donotrecursetofindout,though,butreturn-1ifinconclusive.+*/+staticenumcontains_resultcontains_test(structcommit*candidate,conststructcommit_list*want){-structcommit_list*p;-/* was it previously marked as containing a want commit? */if(candidate->object.flags&TMP_MARK)return1;
@@ -92,26 +100,78 @@ static int contains_recurse(struct commit *candidate,if(candidate->object.flags&UNINTERESTING)return0;/* or are we it? */-if(in_commit_list(want,candidate))+if(in_commit_list(want,candidate)){+candidate->object.flags|=TMP_MARK;return1;+}if(parse_commit(candidate)<0)return0;-/* 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;-return1;-}-}-candidate->object.flags|=UNINTERESTING;-return0;+return-1;}-staticintcontains(structcommit*candidate,conststructcommit_list*want)+/*+*Mimickingtherealstack,thisstacklivesontheheap,avoidingstack+*overflows.+*+*Ateachrecursionstep,thestackitemspointstothecommitswhose+*ancestorsaretobeinspected.+*/+structstack{+intnr,alloc;+structstack_entry{+structcommit*commit;+structcommit_list*parents;+}*stack;+};++staticvoidpush_to_stack(structcommit*candidate,structstack*stack)+{+intindex=stack->nr++;+ALLOC_GROW(stack->stack,stack->nr,stack->alloc);+stack->stack[index].commit=candidate;+stack->stack[index].parents=candidate->parents;+}++staticenumcontains_resultcontains(structcommit*candidate,+conststructcommit_list*want){-returncontains_recurse(candidate,want);+structstackstack={0,0,NULL};+intresult=contains_test(candidate,want);++if(result!=CONTAINS_UNKNOWN)+returnresult;++push_to_stack(candidate,&stack);+while(stack.nr){+structstack_entry*entry=&stack.stack[stack.nr-1];+structcommit*commit=entry->commit;+structcommit_list*parents=entry->parents;++if(!parents){+commit->object.flags|=UNINTERESTING;+stack.nr--;+}+/*+*Ifwejustpoppedthestack,parents->itemhasbeenmarked,+*thereforecontains_testwillreturnameaningful0or1.+*/+elseswitch(contains_test(parents->item,want)){+caseCONTAINS_YES:+commit->object.flags|=TMP_MARK;+stack.nr--;+break;+caseCONTAINS_NO:+entry->parents=parents->next;+break;+caseCONTAINS_UNKNOWN:+push_to_stack(parents->item,&stack);+break;+}+}+free(stack.stack);+returncontains_test(candidate,want);}staticvoidshow_tag_lines(constunsignedchar*sha1,intlines)
@@ -1423,4 +1423,27 @@ EOFtest_cmpexpectactual'+ulimit_stack="ulimit -s 64"+test_lazy_prereqULIMIT'bash -c "'"$ulimit_stack"'"'++>expect+# we require bash and ulimit, this excludes Windows+test_expect_successULIMIT'--contains works in a deep repo''+i=1&&+whiletest$i-lt4000+do+echo"commit refs/heads/master+committerAUThor<author@example.com>$((1000000000+$i*100))+0200+data<<EOF+commit#$i+EOF"+test$i=1&&echo"from refs/heads/master^0"+i=$(($i+1))+done|gitfast-import&&+gitcheckoutmaster&&+gittagfar-far-awayHEAD^&&+bash-c"'"$ulimit_stack"' && git tag --contains HEAD >actual"&&+test_cmpexpectactual+'+ test_done
From: Jeff King <hidden> Date: 2016-06-15 23:00:48
On Wed, Apr 23, 2014 at 09:53:25AM +0200, Stepan Kasal wrote:
I have found out that "ulimit -s" does not work on Windows.
Adding this as a prerequisite, we will skip the test there.
I found this bit weird, as the test originated on Windows. Did it never
actually cause a failure there (i.e., the "ulimit -s" doesn't do
anything)? Or does "ulimit" fail?
-Peff
From: Johannes Schindelin <hidden> Date: 2016-06-15 23:00:48
Hi Peff,
On Wed, 23 Apr 2014, Jeff King wrote:
On Wed, Apr 23, 2014 at 09:53:25AM +0200, Stepan Kasal wrote:
quoted
I have found out that "ulimit -s" does not work on Windows. Adding
this as a prerequisite, we will skip the test there.
I found this bit weird, as the test originated on Windows. Did it never
actually cause a failure there (i.e., the "ulimit -s" doesn't do
anything)? Or does "ulimit" fail?
I must have forgotten to test on Windows. For performance reasons (you
know that I only have a Git time budget of about 15min/day), I developed
the test and the patch on Linux.
Ciao,
Johannes