From 92ce5df9494bf3586e830e9f95a4e98a5d3d6c85 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Wed, 4 Jan 2023 12:01:12 -0600 Subject: [PATCH] Added tests for theoretical bound on tree height and recovery from delete-all - Unless there is a bug, rbyd trees should be strictly <= (2*log2(n)+1) in height. The extra +1 from traditional red-black trees is due to the introduced to-be-pruned alt, but since we clean those up as soon as we can, only one will ever exist in any search path. This also holds true with range deletion, however the definition of n changes to the number of tree operations. This is the same for tombstoning. - Delete-all recovery is a bit tricky because we have no tree at that point, which is weird for an append-only data-structure. --- lfs_util.h | 6 + tests/test_rbyd.toml | 257 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 258 insertions(+), 5 deletions(-) diff --git a/lfs_util.h b/lfs_util.h index b90433ae..fb4c82e7 100644 --- a/lfs_util.h +++ b/lfs_util.h @@ -139,6 +139,12 @@ static inline uint32_t lfs_npw2(uint32_t a) { #endif } +// TODO we should eventually adopt this as the new name for npw2 +// Find the ceiling of log base 2 of the given number +static inline uint32_t lfs_nlog2(uint32_t a) { + return lfs_npw2(a); +} + // Count the number of trailing binary zeros in a // lfs_ctz(0) may be undefined static inline uint32_t lfs_ctz(uint32_t a) { diff --git a/tests/test_rbyd.toml b/tests/test_rbyd.toml index bb52a7f7..3a4eac20 100644 --- a/tests/test_rbyd.toml +++ b/tests/test_rbyd.toml @@ -1577,6 +1577,9 @@ code = ''' lfs_off_t off; lfs_size_t size; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint8_t perm[N]; unsigned stack[N]; @@ -1618,6 +1621,9 @@ code = ''' => LFS_MKRTAG(UATTR, j+1, 0); } + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -1636,6 +1642,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_multi_permutations] @@ -1658,6 +1673,9 @@ code = ''' lfs_off_t off; lfs_size_t size; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint8_t perm[N]; unsigned stack[N]; @@ -1695,6 +1713,9 @@ code = ''' => LFS_MKRTAG(UATTR, j+1, 0); } + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -1713,6 +1734,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_large] @@ -1891,6 +1921,9 @@ code = ''' lfs_off_t off; lfs_size_t size; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint8_t perm[N]; unsigned stack[N]; @@ -1954,6 +1987,9 @@ code = ''' assert(tag == LFS_MKRTAG(UATTR, k+1, 0)); } } + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } // next permutation using Heap's algorithm @@ -1974,6 +2010,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_remove_all] @@ -2077,6 +2122,9 @@ code = ''' lfs_off_t off; lfs_size_t size; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // create one consistent block rbyd = init_rbyd; lfs_bd_erase(&lfs, rbyd.block) => 0; @@ -2133,6 +2181,24 @@ code = ''' => LFS_ERR_NOENT; } + // try resuming from all tags being removed + lfs_rbyd_commit(&lfs, &rbyd, + LFS_MKRATTR(UATTR, 1, 0, "\xaa\xaa\xaa\xaa\xaa\xaa", 6, + NULL)) => 0; + + lfs_rbyd_fetch(&lfs, &rbyd, rbyd.block, NULL) => 0; + lfs_rbyd_lookup(&lfs, &rbyd, + LFS_MKRTAG(UATTR, 1, 0), &off, &size) + => LFS_MKRTAG(UATTR, 1, 0); + for (unsigned j = 1; j < N; j++) { + lfs_rbyd_lookup(&lfs, &rbyd, + LFS_MKRTAG(UATTR, j+1, 0), &off, &size) + => LFS_ERR_NOENT; + } + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -2151,6 +2217,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + N + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_remove_append_permutations] @@ -2173,6 +2248,9 @@ code = ''' lfs_off_t off; lfs_size_t size; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint8_t perm[N]; unsigned stack[N]; @@ -2249,6 +2327,9 @@ code = ''' assert(size == 4); } } + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } } @@ -2270,6 +2351,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + 1 + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' @@ -2612,6 +2702,9 @@ code = ''' }; uint8_t buffer[4]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -2662,6 +2755,9 @@ code = ''' assert(memcmp(buffer, names[j % 6], 4) == 0); } + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -2680,6 +2776,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_multi_create_permutations] @@ -2709,6 +2814,9 @@ code = ''' }; uint8_t buffer[4]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -2755,6 +2863,9 @@ code = ''' assert(memcmp(buffer, names[j % 6], 4) == 0); } + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -2773,6 +2884,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_create_large] @@ -3275,6 +3395,9 @@ code = ''' }; uint8_t buffer[4]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -3347,6 +3470,9 @@ code = ''' lfs_rbyd_get(&lfs, &rbyd, LFS_MKRTAG(CREATEREG, 0, N-1+1), buffer, 4) => LFS_ERR_NOENT; + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } // next permutation using Heap's algorithm @@ -3367,6 +3493,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_delete_range_permutations] @@ -3396,6 +3531,9 @@ code = ''' }; uint8_t buffer[4]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -3481,6 +3619,9 @@ code = ''' lfs_rbyd_get(&lfs, &rbyd, LFS_MKRTAG(UATTR, 1, N-1+1), buffer, 4) => LFS_ERR_NOENT; + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } // next permutation using Heap's algorithm @@ -3501,6 +3642,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + 2*N + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_delete_all] @@ -3771,7 +3921,10 @@ code = ''' "\xee\xee\xee\xee", "\xff\xff\xff\xff", }; - uint8_t buffer[4]; + uint8_t buffer[6]; + + // keep track of the worst case log size + lfs_size_t worst_size = 0; // create one consistent block rbyd = init_rbyd; @@ -3839,6 +3992,25 @@ code = ''' LFS_MKRTAG(CREATEREG, 0, 1), buffer, 4) => LFS_ERR_NOENT; + // try resuming from all tags being removed + lfs_rbyd_commit(&lfs, &rbyd, + LFS_MKRATTR(CREATEREG, 0, 1, "\xaa\xaa\xaa\xaa\xaa\xaa", 6, + NULL)) => 0; + assert(rbyd.count == 1); + + lfs_rbyd_fetch(&lfs, &rbyd, rbyd.block, NULL) => 0; + assert(rbyd.count == 1); + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(CREATEREG, 0, 1), buffer, 6) + => 6; + assert(memcmp(buffer, "\xaa\xaa\xaa\xaa\xaa\xaa", 6) == 0); + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(CREATEREG, 0, 2), buffer, 6) + => LFS_ERR_NOENT; + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -3857,6 +4029,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + 2*N + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_delete_all_range_permutations] @@ -3884,7 +4065,10 @@ code = ''' "\xee\xee\xee\xee", "\xff\xff\xff\xff", }; - uint8_t buffer[4]; + uint8_t buffer[6]; + + // keep track of the worst case log size + lfs_size_t worst_size = 0; // create one consistent block rbyd = init_rbyd; @@ -3953,6 +4137,33 @@ code = ''' LFS_MKRTAG(CREATEREG, 0, 1), buffer, 4) => LFS_ERR_NOENT; + // try resuming from all tags being removed + lfs_rbyd_commit(&lfs, &rbyd, + LFS_MKRATTR(CREATEREG, 0, 1, "\xaa\xaa\xaa\xaa\xaa\xaa", 6, + LFS_MKRATTR(UATTR, 1, 1, "\xaa\xaa\xaa", 3, + NULL))) => 0; + assert(rbyd.count == 1); + + lfs_rbyd_fetch(&lfs, &rbyd, rbyd.block, NULL) => 0; + assert(rbyd.count == 1); + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(CREATEREG, 0, 1), buffer, 6) + => 6; + assert(memcmp(buffer, "\xaa\xaa\xaa\xaa\xaa\xaa", 6) == 0); + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(UATTR, 1, 1), buffer, 6) + => 3; + assert(memcmp(buffer, "\xaa\xaa\xaa", 3) == 0); + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(CREATEREG, 0, 2), buffer, 6) + => LFS_ERR_NOENT; + lfs_rbyd_get(&lfs, &rbyd, + LFS_MKRTAG(UATTR, 1, 2), buffer, 6) + => LFS_ERR_NOENT; + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); + // next permutation using Heap's algorithm if (stack[i] < i) { if (i % 2 == 0) { @@ -3971,10 +4182,16 @@ code = ''' i += 1; } } -''' -# TODO test delete-to-zero with fetches after explicitly? -# TODO also remove-to-zero for removes + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + 2*N + N + 2; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); +''' [cases.test_rbyd_delete_create_permutations] defines.N = 'range(1, 6)' @@ -4003,6 +4220,9 @@ code = ''' }; uint8_t buffer[6]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -4091,6 +4311,9 @@ code = ''' assert(memcmp(buffer, names[expected % 6], 4) == 0); } } + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } } @@ -4112,6 +4335,15 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + 1 + 1; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); ''' [cases.test_rbyd_delete_create_range_permutations] @@ -4141,6 +4373,9 @@ code = ''' }; uint8_t buffer[6]; + // keep track of the worst case log size + lfs_size_t worst_size = 0; + // test all permutations of a given size uint16_t perm[N]; unsigned stack[N]; @@ -4248,6 +4483,9 @@ code = ''' assert(memcmp(buffer, names[expected % 6], 2) == 0); } } + + // keep track of the worst size + worst_size = lfs_max(worst_size, rbyd.off); } } @@ -4269,4 +4507,13 @@ code = ''' i += 1; } } + + // test that tree is self-balancing, we should be strictly bounded + // by height <= 2*log(n)+1, assume tags are roughly ~8 bytes + lfs_size_t n = 1 + N + 1 + 2; + printf("worst size: %u B (N=%u, estimate=%u)\n", + worst_size, n, 8*n*(2*lfs_nlog2(n)+1)); + printf("avg height: %u B (N=%u, estimate=%u)\n", + worst_size / n, n, 8*(2*lfs_nlog2(n)+1)); + assert(worst_size / n <= 8*(2*lfs_nlog2(n)+1)); '''