diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 68339400e..d2714ac15 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -22,6 +22,7 @@ if(applications ) # cmake-format: off set(applications_sources + ns3tcp/ns3tcp-cubic-test-suite.cc ns3tcp/ns3tcp-loss-test-suite.cc ns3tcp/ns3tcp-no-delay-test-suite.cc ns3tcp/ns3tcp-socket-test-suite.cc diff --git a/src/test/ns3tcp/ns3tcp-cubic-test-suite.cc b/src/test/ns3tcp/ns3tcp-cubic-test-suite.cc new file mode 100644 index 000000000..66310c88d --- /dev/null +++ b/src/test/ns3tcp/ns3tcp-cubic-test-suite.cc @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2010 University of Washington (trace writing) + * Copyright (c) 2018-20 NITK Surathkal (topology setup) + * Copyright (c) 2024 Tom Henderson (test definition) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +// Test suite based on tcp-bbr-example.cc modified topology setup: +// +// 1 Gbps 50/100Mbps 1 Gbps +// Sender -------------- R1 -------------- R2 -------------- Receiver +// 5us baseRtt/2 5us +// +// This is a test suite written initially to test the addition of +// TCP friendliness but can also be extended for other Cubic tests. By +// changing some compile-time constants below, it can be used to also produce +// gnuplots and pcaps that demonstrate the behavior. See the WRITE_PCAP and +// WRITE_GNUPLOT booleans below to enable this output. +// +// There are four cases configured presently. +// 1. A Cubic TCP on a 5 ms RTT bottleneck, with no fast convergence nor +// TCP friendliness configured +// 2. A Cubic TCP on a 5 ms RTT bottleneck, with fast convergence but no +// TCP friendliness configured. This exhibits a different cwnd plot from 1. +// 3. A Cubic TCP on a 20 ms RTT bottleneck, with fast convergence but no +// TCP friendliness configured. There is a step change increase in +// bottleneck link capacity but the TCP is slow to exploit it. +// 4. A Cubic TCP on a 20 ms RTT bottleneck, with fast convergence and +// TCP friendliness configured. There is a step change increase in +// bottleneck link capacity and the TCP responds more quickly than in 3. + +#include "ns3/abort.h" +#include "ns3/bulk-send-helper.h" +#include "ns3/config.h" +#include "ns3/data-rate.h" +#include "ns3/error-model.h" +#include "ns3/gnuplot.h" +#include "ns3/inet-socket-address.h" +#include "ns3/internet-stack-helper.h" +#include "ns3/ipv4-address-helper.h" +#include "ns3/ipv4-global-routing-helper.h" +#include "ns3/log.h" +#include "ns3/node-container.h" +#include "ns3/output-stream-wrapper.h" +#include "ns3/packet-sink-helper.h" +#include "ns3/pcap-file.h" +#include "ns3/point-to-point-helper.h" +#include "ns3/point-to-point-net-device.h" +#include "ns3/pointer.h" +#include "ns3/simulator.h" +#include "ns3/string.h" +#include "ns3/tcp-header.h" +#include "ns3/tcp-socket-factory.h" +#include "ns3/test.h" +#include "ns3/trace-helper.h" +#include "ns3/traffic-control-helper.h" +#include "ns3/uinteger.h" + +#include + +using namespace ns3; + +NS_LOG_COMPONENT_DEFINE("Ns3TcpCubicTest"); + +static constexpr bool WRITE_PCAP = false; //!< Set to true to write out pcap. +static constexpr bool WRITE_GNUPLOT = false; //!< Set to true to write out gnuplot. + +/** + * \ingroup system-tests-tcp + */ + +/** + * Add sample trace values to data structures + * \param gnuplotTimeSeries Gnuplot data structure + * \param timeSeries time series of cwnd changes + * \param oldval old value of cwnd + * \param newval new value of cwnd + */ +void +CubicCwndTracer(Gnuplot2dDataset* gnuplotTimeSeries, + std::map* timeSeries, + uint32_t oldval, + uint32_t newval) +{ + // We store data in two structures because Gnuplot2DDataset data is not exportable + NS_LOG_DEBUG("cwnd " << newval / 1448.0); + gnuplotTimeSeries->Add(Simulator::Now().GetSeconds(), newval / 1448.0); + timeSeries->insert_or_assign(Simulator::Now(), newval / 1448.0); +} + +/** + * Test Cubic response + */ +class Ns3TcpCubicTestCase : public TestCase +{ + public: + /** + * Constructor. + * + * \param testCase testcase name + * \param prefix filename prefix if writing output files + * \param fastConvergence whether to enable fast convergence + * \param tcpFriendliness whether to enable TCP friendliness + * \param baseRtt base RTT to use for test case + * \param capacityIncrease whether to trigger a sudden capacity increase + */ + Ns3TcpCubicTestCase(std::string testCase, + std::string prefix, + bool fastConvergence, + bool tcpFriendliness, + Time baseRtt, + bool capacityIncrease); + ~Ns3TcpCubicTestCase() override; + + private: + void DoRun() override; + + /** + * Connect TCP cwnd trace after socket is instantiated + * + * \param nodeId node ID to connect to + * \param socketId socket ID to connect to + */ + void ConnectCwndTrace(uint32_t nodeId, uint32_t socketId); + + /** + * Increases the device bandwidth to 100 Mbps + * \param device device to modify + */ + void IncreaseBandwidth(Ptr device); + + /** + * Check that time series values within a time range are within a value range. + * \param start start of time range + * \param end end of time range + * \param lowerBound lower bound of acceptable values + * \param upperBound upper bound of acceptable values + * \return true if values are within range + */ + bool CheckValues(Time start, Time end, double lowerBound, double upperBound); + + bool m_writeResults{WRITE_PCAP}; //!< Whether to write pcaps + bool m_writeGnuplot{WRITE_GNUPLOT}; //!< Whether to write gnuplot files + Gnuplot2dDataset m_cwndTimeSeries; //!< cwnd time series + std::map m_timeSeries; //!< time series to check + std::string m_prefix; //!< filename prefix if writing files + bool m_fastConvergence; //!< whether to enable fast convergence + bool m_tcpFriendliness; //!< whether to enable TCP friendliness + Time m_baseRtt; //!< the base RTT to use + bool m_capacityIncrease; //!< whether to trigger a capacity increase +}; + +Ns3TcpCubicTestCase::Ns3TcpCubicTestCase(std::string testCase, + std::string prefix, + bool fastConvergence, + bool tcpFriendliness, + Time baseRtt, + bool capacityIncrease) + : TestCase(testCase), + m_prefix(prefix), + m_fastConvergence(fastConvergence), + m_tcpFriendliness(tcpFriendliness), + m_baseRtt(baseRtt), + m_capacityIncrease(capacityIncrease) +{ +} + +Ns3TcpCubicTestCase::~Ns3TcpCubicTestCase() +{ +} + +void +Ns3TcpCubicTestCase::IncreaseBandwidth(Ptr device) +{ + device->SetDataRate(DataRate("100Mbps")); +} + +void +Ns3TcpCubicTestCase::ConnectCwndTrace(uint32_t nodeId, uint32_t socketId) +{ + Config::ConnectWithoutContext( + "/NodeList/" + std::to_string(nodeId) + "/$ns3::TcpL4Protocol/SocketList/" + + std::to_string(socketId) + "/CongestionWindow", + MakeBoundCallback(&CubicCwndTracer, &m_cwndTimeSeries, &m_timeSeries)); +} + +bool +Ns3TcpCubicTestCase::CheckValues(Time start, Time end, double lowerBound, double upperBound) +{ + auto itStart = m_timeSeries.lower_bound(start); + auto itEnd = m_timeSeries.upper_bound(end); + for (auto it = itStart; it != itEnd; it++) + { + if (it->second < lowerBound || it->second > upperBound) + { + NS_LOG_DEBUG("Value " << it->second << " at time " << it->first.GetSeconds() + << " outside range (" << lowerBound << "," << upperBound << ")"); + return false; + } + } + return true; +} + +void +Ns3TcpCubicTestCase::DoRun() +{ + NS_LOG_DEBUG("Running " << m_prefix); + // The maximum bandwidth delay product is 20 ms (base RTT) times 100 Mbps + // which works out to 250KB. Setting the socket buffer sizes to four times + // that value (1MB) should be more than enough. + Config::SetDefault("ns3::TcpSocket::SndBufSize", UintegerValue(1000000)); + Config::SetDefault("ns3::TcpSocket::RcvBufSize", UintegerValue(1000000)); + Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(10)); + Config::SetDefault("ns3::TcpSocket::DelAckCount", UintegerValue(2)); + Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(1448)); + Config::SetDefault("ns3::TcpSocketState::EnablePacing", BooleanValue(true)); + Config::SetDefault("ns3::TcpSocketState::PaceInitialWindow", BooleanValue(true)); + Config::SetDefault("ns3::TcpCubic::FastConvergence", BooleanValue(m_fastConvergence)); + Config::SetDefault("ns3::TcpCubic::TcpFriendliness", BooleanValue(m_tcpFriendliness)); + + Time stopTime = Seconds(20); + + NodeContainer sender; + NodeContainer receiver; + NodeContainer routers; + sender.Create(1); + receiver.Create(1); + routers.Create(2); + + // Create the point-to-point link helpers + PointToPointHelper bottleneckLink; + // With the DynamicQueueLimits setting below, the following statement + // will minimize the buffering in the device and force most buffering + // into the AQM + bottleneckLink.SetQueue("ns3::DropTailQueue", "MaxSize", StringValue("2p")); + if (m_capacityIncrease) + { + // Start at a lower capacity, and increase later + bottleneckLink.SetDeviceAttribute("DataRate", StringValue("50Mbps")); + } + else + { + bottleneckLink.SetDeviceAttribute("DataRate", StringValue("100Mbps")); + } + bottleneckLink.SetChannelAttribute("Delay", TimeValue(m_baseRtt / 2)); + + PointToPointHelper edgeLink; + edgeLink.SetDeviceAttribute("DataRate", StringValue("1000Mbps")); + edgeLink.SetChannelAttribute("Delay", StringValue("5us")); + + // Create NetDevice containers + NetDeviceContainer senderEdge = edgeLink.Install(sender.Get(0), routers.Get(0)); + NetDeviceContainer r1r2 = bottleneckLink.Install(routers.Get(0), routers.Get(1)); + NetDeviceContainer receiverEdge = edgeLink.Install(routers.Get(1), receiver.Get(0)); + + // Install Stack + InternetStackHelper internet; + internet.Install(sender); + internet.Install(receiver); + internet.Install(routers); + + // Configure the root queue discipline + TrafficControlHelper tch; + tch.SetRootQueueDisc("ns3::FqCoDelQueueDisc"); + tch.SetQueueLimits("ns3::DynamicQueueLimits"); + + tch.Install(senderEdge); + tch.Install(receiverEdge); + + // Assign IP addresses + Ipv4AddressHelper ipv4; + ipv4.SetBase("10.0.0.0", "255.255.255.0"); + + Ipv4InterfaceContainer i1i2 = ipv4.Assign(r1r2); + ipv4.NewNetwork(); + Ipv4InterfaceContainer is1 = ipv4.Assign(senderEdge); + ipv4.NewNetwork(); + Ipv4InterfaceContainer ir1 = ipv4.Assign(receiverEdge); + + // Populate routing tables + Ipv4GlobalRoutingHelper::PopulateRoutingTables(); + + // Select sender side port + uint16_t port = 50001; + + // Install application on the sender + BulkSendHelper source("ns3::TcpSocketFactory", InetSocketAddress(ir1.GetAddress(1), port)); + source.SetAttribute("MaxBytes", UintegerValue(0)); + ApplicationContainer sourceApps = source.Install(sender.Get(0)); + sourceApps.Start(Seconds(0.1)); + // Hook trace source after application starts + Simulator::Schedule(Seconds(0.1) + MilliSeconds(1), + &Ns3TcpCubicTestCase::ConnectCwndTrace, + this, + 0, + 0); + sourceApps.Stop(stopTime); + + // Install application on the receiver + PacketSinkHelper sink("ns3::TcpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), port)); + ApplicationContainer sinkApps = sink.Install(receiver.Get(0)); + sinkApps.Start(Seconds(0.0)); + sinkApps.Stop(stopTime); + + if (m_capacityIncrease) + { + // Double the capacity of the bottleneck link at 10 seconds + Ptr device = r1r2.Get(0)->GetObject(); + Simulator::Schedule(Seconds(10), &Ns3TcpCubicTestCase::IncreaseBandwidth, this, device); + } + + std::ostringstream oss; + oss << m_prefix; + if (m_writeResults) + { + edgeLink.EnablePcap(oss.str(), senderEdge.Get(0)); + edgeLink.EnablePcap(oss.str(), receiverEdge.Get(0)); + edgeLink.EnableAscii(oss.str(), senderEdge.Get(0)); + edgeLink.EnableAscii(oss.str(), receiverEdge.Get(0)); + } + Simulator::Stop(Seconds(30)); + Simulator::Run(); + + if (m_writeGnuplot) + { + std::ofstream outfile(oss.str() + ".plt"); + + std::string capacityIncreaseString; + if (m_capacityIncrease) + { + capacityIncreaseString = " capacity increase = 1"; + } + Gnuplot gnuplot = Gnuplot( + oss.str() + ".eps", + "Cubic concave/convex response (" + std::to_string(m_baseRtt.GetMilliSeconds()) + + " ms RTT, 1448 byte segs, 100 Mbps bottleneck)\\nFast convergence = " + + std::to_string(m_fastConvergence) + + " friendliness = " + std::to_string(m_tcpFriendliness) + capacityIncreaseString); + gnuplot.SetTerminal("post eps color enhanced"); + gnuplot.SetLegend("Time (seconds)", "Cwnd (segments)"); + m_cwndTimeSeries.SetTitle(""); + gnuplot.AddDataset(m_cwndTimeSeries); + gnuplot.GenerateOutput(outfile); + } + + // Check that cwnd values are within tolerance of expected values + // The expected values were determined by inspecting the plots generated + if (m_prefix == "ns3-tcp-cubic-no-heuristic") + { + // Check overall min and max + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(1), Seconds(19), 50, 90), + true, + "cwnd outside range"); + // Time just before a reduction does not have much variation + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(9), Seconds(9.7), 84, 89), + true, + "cwnd outside range"); + } + else if (m_prefix == "ns3-tcp-cubic-fast-conv") + { + // Check overall min and max + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(1), Seconds(19), 50, 90), + true, + "cwnd outside range"); + // Initial convex region does not have much variation + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(4), Seconds(6), 83, 88), + true, + "cwnd outside range"); + } + else if (m_prefix == "ns3-tcp-cubic-no-friendly") + { + // Between time 12 and 16, cwnd should be fairly constant + // because without TCP friendliness, Cubic does not respond quickly + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(12), Seconds(16), 107, 123), + true, + "cwnd outside range"); + // After time 19.5, cwnd should have grown much higher + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(19.5), Seconds(20), 180, 210), + true, + "cwnd outside range"); + } + else if (m_prefix == "ns3-tcp-cubic-friendly") + { + // In contrast to previous case, cwnd should grow above 150 much sooner + NS_TEST_ASSERT_MSG_EQ(CheckValues(Seconds(12), Seconds(14), 150, 210), + true, + "cwnd outside range"); + } + + Simulator::Destroy(); +} + +/** + * \ingroup tcp-cubic-tests + * TestSuite for module tcp-cubic + */ +class Ns3TcpCubicTestSuite : public TestSuite +{ + public: + Ns3TcpCubicTestSuite(); +}; + +Ns3TcpCubicTestSuite::Ns3TcpCubicTestSuite() + : TestSuite("ns3-tcp-cubic", UNIT) +{ + // Test Cubic with no fast convergence or TCP friendliness enabled + // This results in a cwnd plot that has only the concave portion of + // returning cwnd to Wmax + AddTestCase(new Ns3TcpCubicTestCase("no heuristic", + "ns3-tcp-cubic-no-heuristic", + false, + false, + MilliSeconds(5), + false), + TestCase::QUICK); + + // Test Cubic with fast convergence but no TCP friendliness enabled + // This results in a cwnd plot that has concave and convex regions, as + // well as convex-only regions; also observed on Linux testbeds + AddTestCase(new Ns3TcpCubicTestCase("fast convergence on", + "ns3-tcp-cubic-fast-conv", + true, + false, + MilliSeconds(5), + false), + TestCase::QUICK); + + // Test Cubic with fast convergence but no TCP friendliness enabled + // with a higher RTT (20ms) and a step change in capacity at time 10s. + // This results in a sluggish response by TCP Cubic to use the new capacity. + AddTestCase(new Ns3TcpCubicTestCase("fast convergence on, Reno friendliness off", + "ns3-tcp-cubic-no-friendly", + true, + false, + MilliSeconds(20), + true), + TestCase::QUICK); + + // Test Cubic with fast convergence but with TCP friendliness enabled + // with a higher RTT (20ms) and a step change in capacity at time 10s. + // This results in a faster response by TCP Cubic to use the new capacity. + AddTestCase(new Ns3TcpCubicTestCase("fast convergence on, Reno friendliness on", + "ns3-tcp-cubic-friendly", + true, + true, + MilliSeconds(20), + true), + TestCase::QUICK); +} + +/** + * \ingroup tcp-cubic-tests + * Static variable for test initialization + */ +static Ns3TcpCubicTestSuite ns3TcpCubicTestSuite;