-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce MergingRunData and MergingTreeData #592
Conversation
bff20b9
to
7188e2a
Compare
"the tests" meaning the generator invariant tests, I take it? |
Mainly all the shrinker invariant tests, but for Instead of decreasing the size of these particular tests, I should probably change the instance to generate smaller trees, since any property on trees will otherwise be quite slow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks useful!
The tests are quite slow even after halving the maximum size. The main issue is that RunData has a lot of possible shrinks (sometimes over 100k), which gets even worse when there are dozens of them in the tree.
Normally with shrinking test cases it's not extremely important to not generate too many shrinks. Rather, the ordering of shrunk values is more important, so that we don't evaluate too many before we find the next, smaller, failing test case. But yeah, it would be nice to reduce the number of shrink values for these "arbitrary/shrinking preserve invariant" tests. I've commented elsewhere that we could maybe reduce the tree size by generating fewer children in the merging tree
= QC.frequency | ||
[ (1, do | ||
-- pending level merge without child | ||
preExisting <- QC.vectorOf (n - 1) $ -- 1 for constructor itself | ||
genPreExistingRunData genKey genVal genBlob | ||
return (PendingLevelMergeData preExisting Nothing)) | ||
, (1, do | ||
-- pending level merge with child | ||
numPreExisting <- QC.chooseInt (0, min 20 (n - 2)) | ||
preExisting <- QC.vectorOf numPreExisting $ | ||
genPreExistingRunData genKey genVal genBlob | ||
tree <- go (n - numPreExisting - 1) | ||
return (PendingLevelMergeData preExisting (Just tree))) | ||
, (2, do | ||
-- pending union merge | ||
ns <- QC.shuffle =<< arbitraryPartition2 n | ||
PendingUnionMergeData <$> traverse go ns) | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about the number of children: would we test most interesting cases with 2
or 3
children?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it does indeed matter and I landed on distribution between 0 and 6 pre-existing run, but skewing towards smaller numbers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about the chooseIntSkewed (0, 6)
I went with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No objections.
Out of interest, do you feel there are interesting test cases where we'd need more than 2 or 3 runs? In my mind, 4 or more runs wouldn't do anything more interesting than 2 or 3 runs would do. It's more data mostly, and having fewer runs could speed tests down the line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's anything interesting about having more than 3 runs, but it can make some cases more likely, like having the same key in more than 2 runs. I can play around with it once more to see how much it affects test runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has a noticeable, but relatively small effect. Changing the range to (0, 3)
lead to about 25% faster tests. I decided to generate 50% shorter runs instead (see last commit), more than halving the runtime. Not sure it was necessary, but ~17s for all MergingTreeData
tests seems more tolerable to me than ~35s.
352840f
to
bff6984
Compare
bff6984
to
442e8a8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
unsafeCreateMergingTree hfs hbio resolve indexType path counter = go | ||
where | ||
go = \case |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can inline the go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can inline it, but then I need to pass all the arguments to each recursive call, which is kind of noisy and often requires a line break.
= QC.frequency | ||
[ (1, do | ||
-- pending level merge without child | ||
preExisting <- QC.vectorOf (n - 1) $ -- 1 for constructor itself | ||
genPreExistingRunData genKey genVal genBlob | ||
return (PendingLevelMergeData preExisting Nothing)) | ||
, (1, do | ||
-- pending level merge with child | ||
numPreExisting <- QC.chooseInt (0, min 20 (n - 2)) | ||
preExisting <- QC.vectorOf numPreExisting $ | ||
genPreExistingRunData genKey genVal genBlob | ||
tree <- go (n - numPreExisting - 1) | ||
return (PendingLevelMergeData preExisting (Just tree))) | ||
, (2, do | ||
-- pending union merge | ||
ns <- QC.shuffle =<< arbitraryPartition2 n | ||
PendingUnionMergeData <$> traverse go ns) | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No objections.
Out of interest, do you feel there are interesting test cases where we'd need more than 2 or 3 runs? In my mind, 4 or more runs wouldn't do anything more interesting than 2 or 3 runs would do. It's more data mostly, and having fewer runs could speed tests down the line
This keeps the runtime of the shrinker tests around 10s with only a small impact on the distribution of generated tests. There is still a decent amount of somewhat long runs (max size is now 50, not 100). Before: run length (1249 in total): 63.23% 10 <= n < 100 36.17% 1 <= n < 10 0.59% n == 0 After: run length (1181 in total): 51.82% 10 <= n < 100 45.39% 1 <= n < 10 2.79% n == 0
442e8a8
to
d78f451
Compare
Description
Similarly to the existing
RunData
, these are simplified versions ofMergingRun
andMergingTree
, which can be generated, shrunk and turned into their full on-disk versions.I also added labelling and tests for the generators and shrinkers, as well as the code creating the
MergingTree
etc. from the data.The tests are quite slow even after halving the maximum size. The main issue is that
RunData
has a lot of possible shrinks (sometimes over 100k), which gets even worse when there are dozens of them in the tree.test output