private static void Test() { // Make a Random instance with a fixed seed so that tests are repeatable. Random random = new Random(0); DisjointSet test = null; // Add a single range. DisjointSet.Add(ref test, new Range(5, 10)); // Verify that DisjointSet.Contains works for each value in the range. for (ulong i = 0; i <= 15; i++) { Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ (5 <= i && i <= 10)), "DisjointSet.Contains does not work (simple case)."); } // Verify that it works even when the set is splayed randomly. for (ulong j = 0; j <= 100; j++) { ulong i = (ulong)random.Next(0, 15); Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ (5 <= i && i <= 10)), "DisjointSet.Contains does not work (simple case, random splaying)."); } // Add new ranges that overlap the existing range(s). DisjointSet.Add(ref test, new Range(4, 9)); DisjointSet.Add(ref test, new Range(15, 20)); DisjointSet.Add(ref test, new Range(10, 15)); // Verify that the ranges were merged. Debug.Assert(test.Left == null && test.Right == null, "DisjointSet.Add did not merge overlapping ranges."); Debug.Assert(test.Value == new Range(4, 20), "DisjointSet.Add did not merge overlapping ranges."); // Add ranges adjacent to the ends of the existing range. DisjointSet.Add(ref test, new Range(21, 25)); DisjointSet.Add(ref test, 3ul); // Verify that they were also merged. Debug.Assert(test.Left == null && test.Right == null, "DisjointSet.Add did not merge adjacent ranges."); Debug.Assert(test.Value == new Range(3, 25), "DisjointSet.Add did not merge adjacent ranges."); // Remove a range. DisjointSet.Remove(ref test, new Range(10, 15)); // Verify that the ranges still work. for (ulong i = 0; i <= 30; i++) { Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ ((3 <= i && i <= 9) || (16 <= i && i <= 25))), "DisjointSet.Contains does not work after removal."); } // Verify that it works even when the set is splayed randomly. for (ulong j = 0; j <= 300; j++) { ulong i = (ulong)random.Next(0, 30); Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ ((3 <= i && i <= 9) || (16 <= i && i <= 25))), "DisjointSet.Contains does not work after removal (random splaying)."); } // Remove the entire split ranges (on a cloned copy so we can do it several times). DisjointSet empty; empty = test.Clone(); DisjointSet.Remove(ref empty, Range.UNIVERSE); Debug.Assert(empty == null, "DisjointSet.Remove(UNIVERSE) didn't result in an empty set."); empty = test.Clone(); DisjointSet.Remove(ref empty, new Range(3, 25)); Debug.Assert(empty == null, "DisjointSet.Remove of entire range didn't result in an empty set."); empty = test.Clone(); DisjointSet.Remove(ref empty, new Range(3, 26)); Debug.Assert(empty == null, "DisjointSet.Remove of entire range didn't result in an empty set."); empty = test.Clone(); DisjointSet.Remove(ref empty, new Range(2, 25)); Debug.Assert(empty == null, "DisjointSet.Remove of entire range didn't result in an empty set."); ///////////////////////////////////////////////////////////////////////// // Now reset the set so we can do some stress tests (mostly to make // sure the splaying code works correctly). test = null; // Insert all integers divisible by 5, 7, or 11. This makes the ranges // easily testable, and results in a set with some singletons as well // as some contiguous ranges. for (ulong i = 0; i < 1000; i++) { if (i % 5 == 0 || i % 7 == 0 || i % 11 == 0) { DisjointSet.Add(ref test, i); } } // Verify the contents of the set. for (ulong i = 0; i < 1000; i++) { Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ (i % 5 == 0 || i % 7 == 0 || i % 11 == 0)), "DisjointSet.Contains failed the stress test."); } // Verify the contents of the set, even with random splaying. for (ulong j = 0; j < 10000; j++) { ulong i = (ulong)random.Next(0, 1000); Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ (i % 5 == 0 || i % 7 == 0 || i % 11 == 0)), "DisjointSet.Contains failed the stress test (random splaying)."); } // Now remove all singletons divisible by 7. for (ulong i = 0; i < 1000; i++) { if (i % 7 == 0) { DisjointSet.Remove(ref test, i); } } // Verify the contents of the set. for (ulong i = 0; i < 1000; i++) { Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ ((i % 5 == 0 || i % 11 == 0) && (i % 7 != 0))), "DisjointSet.Contains failed the stress test."); } // Verify the contents of the set, even with random splaying. for (ulong j = 0, i; j < 10000; j++) { i = (ulong)random.Next(0, 1000); Debug.Assert(!(DisjointSet.Contains(ref test, i) ^ ((i % 5 == 0 || i % 11 == 0) && (i % 7 != 0))), "DisjointSet.Contains failed the stress test (random splaying)."); } // Test the IEnumerator implementation. ArrayList enums = new ArrayList(); ulong prev = 0; foreach (ulong i in test) { Debug.Assert(i >= prev, "The enumeration is not well ordered."); enums.Add(prev = i); } // Verify that the output of the enumerator is exactly what should be in the set. for (ulong i = 0; i < 1000; i++) { Debug.Assert(!(enums.Contains(i) ^ ((i % 5 == 0 || i % 11 == 0) && (i % 7 != 0))), "The enumeration did not properly enumerate the set."); } // Test removing from a big tree. DisjointSet.Remove(ref test, Range.UNIVERSE); Debug.Assert(test == null, "DisjointSet.Remove(UNIVERSE) didn't yield an empty set."); ///////////////////////////////////////////////////////////////////////// // Reset the test and add a lot of small random ranges. This simulates // the kind of behavior experienced by RTPNetworkReceiver. test = null; ArrayList values = new ArrayList(10000); for (ulong i = 0; i < 10000; i++) { Range r = i; // Expand the range by some random number of indices. while (random.Next(2) == 1) { r = new Range(r.Min, ++i); } // Add all the indices in the range to both sets. DisjointSet.Add(ref test, r); for (ulong v = r.Min; v <= r.Max; v++) { values.Add(v); } // Skip some random number of indices. while (random.Next(2) == 1) { i++; } } // Test removing all of the values from the set in randomized ascending order, // just like what happens when receiving responses to a NACK (ie., some frames // might be dropped twice (or more times), and then NACKed again). DisjointSet copy = test.Clone(); for (ArrayList compare = (ArrayList)values.Clone(); compare.Count > 0;) { for (int i = 0; i < compare.Count;) { Debug.Assert(copy != null, "DisjointSet is empty but not all entries have been removed."); // Skip some random number of entries. while (random.Next(2) == 1) { i++; } if (i >= compare.Count) { break; } // Remove the entries one at a time. ulong v = (ulong)compare[i]; DisjointSet.Remove(ref copy, v); compare.RemoveAt(i); } } Debug.Assert(copy == null, "All entries were removed but DisjointSet is not empty."); // Test that all of the entries were actually added to the set. foreach (ulong v in test) { Debug.Assert(values.Contains(v), "DisjointSet contains a value that it shouldn't."); } foreach (ulong v in values) { Debug.Assert(DisjointSet.Contains(ref test, v), "DisjointSet doesn't contain a value that it should."); } ///////////////////////////////////////////////////////////////////////// // Simulate an error-case discovered during actual testing. // See Bug 821, attachment #45 (comment #3). DisjointSet current = null; DisjointSet.Add(ref current, new Range(1, 4)); DisjointSet.Add(ref current, new Range(6, 16)); DisjointSet.Add(ref current, new Range(18, 20)); DisjointSet.Add(ref current, new Range(22, 33)); DisjointSet.Add(ref current, new Range(35, 55)); DisjointSet.Add(ref current, new Range(57, 61)); DisjointSet.Add(ref current, 64); DisjointSet.Add(ref current, 67); DisjointSet stable = null; DisjointSet.Add(ref stable, new Range(1, 16)); DisjointSet.Add(ref stable, new Range(18, 61)); DisjointSet.Add(ref stable, 64); DisjointSet.Add(ref stable, 67); copy = stable.Clone(); test = new Range(1, 100); DisjointSet.Remove(ref test, current); foreach (ulong i in test) { DisjointSet.Remove(ref copy, i); } DisjointSet.Remove(ref copy, 0); DisjointSet.Remove(ref copy, stable); Debug.Assert(copy == null, "Stable set is not a superset of the Current set."); }
/// <summary> /// Attempts to assemble a chunk from a chunked messages, /// and sends a NACK when dropped chunks are detected. /// </summary> /// <param name="chunk">The received chunk to process</param> /// <returns>If the chunk was the last remaining chunk needed to complete a message, /// returns the deserialized message; otherwise, <code>null</code></returns> public IEnumerable <object> Add(Chunk chunk) { if (chunk.FrameSequence > 0) { // Check if we have seen this packet before. If so, do nothing. if (DisjointSet.Contains(ref this.m_ReceivedFrameSequences, chunk.FrameSequence)) { yield break; } DisjointSet.Add(ref this.m_ReceivedFrameSequences, chunk.FrameSequence); // Save space in the ReceivedSequenceNumbers queue by adding all chunks // that we can't expect to receive anymore. if (chunk.OldestRecoverableFrame > ulong.MinValue) { DisjointSet.Add(ref this.m_ReceivedFrameSequences, new Range(ulong.MinValue, chunk.OldestRecoverableFrame - 1)); } // Check the frame sequence number to see if any frames were skipped. // If so, then it is likely that the frame has been dropped, so it is // necessary to send a NACK. if (chunk.FrameSequence > this.m_GreatestFrameSequence + 1ul) { //Debug.WriteLine(string.Format("Frames #{0}-{1} were dropped ({2} total)! Requesting replacements (oldest recoverable is #{3})...", // this.m_GreatestFrameSequence + 1ul, chunk.FrameSequence - 1ul, chunk.FrameSequence - this.m_GreatestFrameSequence - 1, // chunk.OldestRecoverableFrame), this.GetType().ToString()); if (this.Nack != null) { // Send a NACK which requests rebroadcast of all the skipped frames. Range range = new Range( Math.Max(this.m_GreatestFrameSequence + 1ul, chunk.OldestRecoverableFrame), chunk.FrameSequence - 1ul); this.Nack(range); } } // Otherwise, don't do anything special. These two branches are for debugging. else if (chunk.FrameSequence == this.m_GreatestFrameSequence) { // Duplicate chunk received, or else the first chunk ever received. } else if (chunk.FrameSequence < this.m_GreatestFrameSequence && chunk.FrameSequence > ulong.MinValue) { Debug.WriteLine(string.Format("%%% Frame #{0} (message #{1}, chunk #{2} of {3}, {4} bytes) received out of order (off by {5})", chunk.FrameSequence, chunk.MessageSequence, chunk.ChunkSequenceInMessage + 1, chunk.NumberOfChunksInMessage, chunk.Data.Length, this.m_GreatestFrameSequence - chunk.FrameSequence), this.GetType().ToString()); } // Assuming the chunk has not been duplicated or received out of order, // update our variable containing the highest sequence number seen // so far so we know which future frames have been dropped. if (chunk.FrameSequence > this.m_GreatestFrameSequence) { this.m_GreatestFrameSequence = chunk.FrameSequence; } } // If the message depends on a message that has not yet been received, // add it to the queue of chunks waiting for that message. // Then, STOP PROCESSING the message. // Assemble(chunk) will get called by FlushWaitingChunks after // the dependencies are satisfied. if (chunk.MessageDependency > ulong.MinValue && !DisjointSet.Contains(ref this.m_ReceivedMessages, chunk.MessageDependency)) { List <Chunk> waiters; if (!this.m_ChunksWaitingForDependencies.TryGetValue(chunk.MessageDependency, out waiters)) { this.m_ChunksWaitingForDependencies[chunk.MessageDependency] = waiters = new List <Chunk>(); } Debug.WriteLine(string.Format("@@@ Unsatisfied dependency: message #{0} (chunk #{1} of {2}) depends on #{3}; {4} chunks are already waiting.", chunk.MessageSequence, chunk.ChunkSequenceInMessage + 1, chunk.NumberOfChunksInMessage, chunk.MessageDependency, waiters.Count)); waiters.Add(chunk); yield break; } // Flush any dependencies of messages we can no longer expect to recover. // Do this by finding the difference between the range of all unrecoverable // message sequence numbers and the sequence numbers that have been received. // FlushWaitingChunks is called on each of the unsatisfiable sequence numbers. if (chunk.OldestRecoverableMessage > ulong.MinValue) { Range unrecoverable = new Range(ulong.MinValue, chunk.OldestRecoverableMessage - 1); DisjointSet unsatisfiable = unrecoverable; DisjointSet.Remove(ref unsatisfiable, this.m_ReceivedMessages); if (unsatisfiable != null) { foreach (ulong unsatisfied in unsatisfiable) { this.FlushWaitingChunks(unsatisfied); } } DisjointSet.Add(ref this.m_ReceivedMessages, unrecoverable); } // If the dependency is already satisfied (or not specified), // attempt to assemble the chunk into a completed message. foreach (object message in this.Assemble(chunk)) { yield return(message); } }