Skip to content

Commit 532c1f2

Browse files
simplify the reassembler
1 parent 7b5c198 commit 532c1f2

File tree

2 files changed

+40
-86
lines changed

2 files changed

+40
-86
lines changed

libudpard/udpard.c

Lines changed: 35 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -756,52 +756,6 @@ static int32_t rx_cavl_compare_fragment_end(const void* const user, const udpard
756756
return 0; // clang-format on
757757
}
758758

759-
/// True if the fragment tree does not have a contiguous payload coverage in [left, right).
760-
/// This function requires that the tree does not have fully-overlapped fragments; the implications are that
761-
/// no two fragments may have the same offset, and that fragments ordered by offset also order by their ends.
762-
/// The complexity is O(log n + k), where n is the number of fragments in the tree and k is the number of fragments
763-
/// overlapping the specified range, assuming there are no fully-overlapped fragments.
764-
static bool rx_fragment_tree_has_gap_in_range(udpard_tree_t* const root, const size_t left, const size_t right)
765-
{
766-
if (left >= right) {
767-
return false; // empty fragment; no gap by convention
768-
}
769-
udpard_fragment_t* frag = (udpard_fragment_t*)cavl2_predecessor(root, &left, &rx_cavl_compare_fragment_offset);
770-
if (frag == NULL) {
771-
return true;
772-
}
773-
// offset+size can overflow on platforms with 16-bit size and requires special treatment there.
774-
if ((frag->offset + frag->view.size) <= left) { // The predecessor ends before the left edge.
775-
return true;
776-
}
777-
size_t covered = left; // This is the O(k) part. The scan starting point search is O(log n).
778-
while ((frag != NULL) && (frag->offset < right)) {
779-
if (frag->offset > covered) {
780-
return true;
781-
}
782-
covered = larger(covered, frag->offset + frag->view.size);
783-
if (covered >= right) {
784-
return false; // Reached the end of the requested range without gaps.
785-
}
786-
frag = (udpard_fragment_t*)cavl2_next_greater(&frag->index_offset);
787-
}
788-
return covered < right;
789-
}
790-
791-
/// True if the specified fragment should be retained. Otherwise, it is redundant and should be discarded.
792-
/// The complexity is O(log n + k), see rx_fragment_tree_has_gap_in_range() for details.
793-
static bool rx_fragment_is_needed(udpard_tree_t* const root,
794-
const size_t frag_offset,
795-
const size_t frag_size,
796-
const size_t transfer_size,
797-
const size_t extent)
798-
{
799-
const size_t end = smaller3(frag_offset + frag_size, transfer_size, extent);
800-
// We need to special-case zero-size transfers because they are effectively just a single gap at offset zero.
801-
return ((end == 0) && (frag_offset == 0)) ||
802-
((frag_offset < extent) && rx_fragment_tree_has_gap_in_range(root, frag_offset, end));
803-
}
804-
805759
/// Finds the number of contiguous payload bytes received from offset zero after accepting a new fragment.
806760
/// The transfer is considered fully received when covered_prefix >= min(extent, transfer_payload_size).
807761
/// This should be invoked after the fragment tree accepted a new fragment at frag_offset with frag_size.
@@ -841,27 +795,51 @@ static rx_fragment_tree_update_result_t rx_fragment_tree_update(udpard_tree_t**
841795
const size_t extent,
842796
size_t* const covered_prefix_io)
843797
{
844-
// The new fragment is needed in two cases:
845-
// 1) It covers new ground in the [0, extent) range.
846-
// 2) It overlaps smaller fragments that are already in the tree, which can be replaced.
847-
// We do a quick lookup first to see if an existing fragment fully contains the new one;
848-
// this simplifies the logic below an is very fast.
798+
// Do a quick lookup first to see if an existing fragment fully contains the new one or is equal,
799+
// in which case we can immediately discard the new fragment; this simplifies the logic below an is very fast.
800+
// This helps maintain a fundamental invariant of the fragment tree: no fully-contained fragments.
849801
const size_t left = frame.offset;
850802
const size_t right = frame.offset + frame.payload.size;
851803
udpard_fragment_t* frag = (udpard_fragment_t*)cavl2_predecessor(*root, &left, &rx_cavl_compare_fragment_offset);
852804
if ((frag != NULL) && ((frag->offset + frag->view.size) >= right)) {
853805
mem_free_payload(payload_deleter, frame.origin);
854-
return rx_fragment_tree_not_done; // New fragment is fully contained within an existing one.
806+
return rx_fragment_tree_not_done; // New fragment is fully contained within an existing one, discard.
807+
}
808+
809+
// If the new fragment is not contained by another, we accept it even if it doesn't add new payload coverage.
810+
// This may seem counter-intuitive at first, and it is indeed very easy to query the tree to check if there
811+
// is a payload gap in a given range, but the reason we accept such fragments is that we speculate that
812+
// other large fragments may arrive in the future that will join with this one, allowing eviction of the smaller
813+
// ones, thus representing the full payload in a fewer number of fragments.
814+
// Consider the following scenario:
815+
// |--A--|--B--|--C--|--D--| <-- first fragment set
816+
// |---X---|---Y---|---Z---| <-- second fragment set
817+
// Suppose we already have A..D received. Arrival of either X or Z allows eviction of A/D immediately.
818+
// Arrival of Y does not allow an immediate eviction of any fragment, but if we had rejected it because it added
819+
// no new coverage, we would miss the opportunity to evict B/C when X or Z arrive later.
820+
// Therefore, we keep everything that comes our way as long as it is not smaller than what we already have.
821+
//
822+
// Ensure we can allocate the fragment header for the new frame before pruning the tree to avoid data loss.
823+
udpard_fragment_t* mew = mem_alloc(fragment_memory, sizeof(udpard_fragment_t));
824+
if (mew == NULL) {
825+
mem_free_payload(payload_deleter, frame.origin);
826+
return rx_fragment_tree_oom; // Cannot allocate fragment header. Maybe we will succeed later.
855827
}
828+
mem_zero(sizeof(*mew), mew);
829+
mew->view.data = frame.payload.data;
830+
mew->view.size = frame.payload.size;
831+
mew->origin.data = frame.origin.data;
832+
mew->origin.size = frame.origin.size;
833+
mew->offset = frame.offset;
834+
mew->payload_deleter = payload_deleter;
856835

857836
// The addition of a larger fragment that joins adjacent fragments together into a larger contiguous block may
858837
// render smaller fragments crossing its boundaries redundant.
859838
// To check for that, we create a new virtual fragment that represents the new fragment together with those
860839
// that join it on either end, if any, and then look for fragments contained within the virtual one.
861840
// Example:
862-
// |--B--|
841+
// |--A--|--B--|
863842
// |--X--|
864-
// |--A--|
865843
// The addition of fragment A or B will render X redundant, even though it is not contained within either.
866844
// This algorithm will detect that and mark X for removal.
867845
//
@@ -884,39 +862,16 @@ static rx_fragment_tree_update_result_t rx_fragment_tree_update(udpard_tree_t**
884862
}
885863
UDPARD_ASSERT((v_left <= left) && (right <= v_right));
886864

887-
// Find the first victim. It has to be done early to decide if the new fragment is worth keeping at all.
888-
frag = (udpard_fragment_t*)cavl2_lower_bound(*root, &v_left, &rx_cavl_compare_fragment_offset);
889-
const bool need = ((frag != NULL) && ((frag->offset + frag->view.size) <= v_right)) ||
890-
rx_fragment_is_needed(*root, frame.offset, frame.payload.size, transfer_payload_size, extent);
891-
if (!need) {
892-
mem_free_payload(payload_deleter, frame.origin);
893-
return rx_fragment_tree_not_done; // Cannot make use of this fragment.
894-
}
895-
896-
// Ensure we can allocate the fragment header for the new frame before pruning the tree to avoid data loss.
897-
udpard_fragment_t* mew = mem_alloc(fragment_memory, sizeof(udpard_fragment_t));
898-
if (mew == NULL) {
899-
mem_free_payload(payload_deleter, frame.origin);
900-
return rx_fragment_tree_oom; // Cannot allocate fragment header. Maybe we will succeed later.
901-
}
902-
mem_zero(sizeof(*mew), mew);
903-
mew->view.data = frame.payload.data;
904-
mew->view.size = frame.payload.size;
905-
mew->origin.data = frame.origin.data;
906-
mew->origin.size = frame.origin.size;
907-
mew->offset = frame.offset;
908-
mew->payload_deleter = payload_deleter;
909-
910865
// Remove all redundant fragments before inserting the new one.
911-
// No need to repeat tree lookup, we just step through the nodes using the next_greater lookup.
866+
// No need to repeat tree lookup at every iteration, we just step through the nodes using the next_greater lookup.
867+
frag = (udpard_fragment_t*)cavl2_lower_bound(*root, &v_left, &rx_cavl_compare_fragment_offset);
912868
while ((frag != NULL) && (frag->offset >= v_left) && ((frag->offset + frag->view.size) <= v_right)) {
913869
udpard_fragment_t* const next = (udpard_fragment_t*)cavl2_next_greater(&frag->index_offset);
914870
cavl2_remove(root, &frag->index_offset);
915871
mem_free_payload(frag->payload_deleter, frag->origin);
916872
mem_free(fragment_memory, sizeof(udpard_fragment_t), frag);
917873
frag = next;
918874
}
919-
920875
// Insert the new fragment.
921876
udpard_tree_t* const res = cavl2_find_or_insert(root, //
922877
&mew->offset,
@@ -925,8 +880,7 @@ static rx_fragment_tree_update_result_t rx_fragment_tree_update(udpard_tree_t**
925880
&cavl2_trivial_factory);
926881
UDPARD_ASSERT(res == &mew->index_offset);
927882
(void)res;
928-
929-
// Update the covered prefix.
883+
// Update the covered prefix. This requires only a single full scan across all iterations!
930884
*covered_prefix_io = rx_fragment_tree_update_covered_prefix(*root, //
931885
*covered_prefix_io,
932886
frame.offset,

tests/src/test_intrusive_rx.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ static void test_rx_fragment_tree_update_a(void)
8888
TEST_ASSERT_EQUAL_size_t(1, alloc_payload.count_alloc);
8989
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.count_free);
9090
TEST_ASSERT_EQUAL_size_t(0, alloc_payload.count_free);
91-
// Free the tree (as in freedom). The free tree is free to manifest its own destiny.
91+
// Free the tree.
9292
udpard_fragment_free_all((udpard_fragment_t*)root, mem_frag);
9393
// Check the heap.
9494
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.allocated_fragments);
@@ -130,7 +130,7 @@ static void test_rx_fragment_tree_update_a(void)
130130
TEST_ASSERT_EQUAL_size_t(1, alloc_payload.count_alloc);
131131
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.count_free);
132132
TEST_ASSERT_EQUAL_size_t(0, alloc_payload.count_free);
133-
// Free the tree (as in freedom). The free tree is free to manifest its own destiny.
133+
// Free the tree (as in freedom).
134134
udpard_fragment_free_all((udpard_fragment_t*)root, mem_frag);
135135
// Check the heap.
136136
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.allocated_fragments);
@@ -173,7 +173,7 @@ static void test_rx_fragment_tree_update_a(void)
173173
TEST_ASSERT_EQUAL_size_t(1, alloc_payload.count_alloc);
174174
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.count_free);
175175
TEST_ASSERT_EQUAL_size_t(0, alloc_payload.count_free);
176-
// Free the tree (as in freedom). The free tree is free to manifest its own destiny.
176+
// Free the tree (as in freedom).
177177
udpard_fragment_free_all((udpard_fragment_t*)root, mem_frag);
178178
// Check the heap.
179179
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.allocated_fragments);
@@ -246,7 +246,7 @@ static void test_rx_fragment_tree_update_a(void)
246246
TEST_ASSERT_EQUAL_size_t(3, alloc_payload.count_alloc);
247247
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.count_free);
248248
TEST_ASSERT_EQUAL_size_t(0, alloc_payload.count_free);
249-
// Free the tree (as in freedom). The free tree is free to manifest its own destiny.
249+
// Free the tree (as in freedom).
250250
udpard_fragment_free_all((udpard_fragment_t*)root, mem_frag);
251251
// Check the heap.
252252
TEST_ASSERT_EQUAL_size_t(0, alloc_frag.allocated_fragments);
@@ -351,7 +351,7 @@ static void test_rx_fragment_tree_update_a(void)
351351
res = rx_fragment_tree_update(&root, //
352352
mem_frag,
353353
del_payload,
354-
make_frame_base(mem_payload, 1, 2, ":3"),
354+
make_frame_base(mem_payload, 1, 1, "z"),
355355
100,
356356
10,
357357
&cov);

0 commit comments

Comments
 (0)