From 490a1b83e4d7db7ac6523134a74f7ee7f0f3afa2 Mon Sep 17 00:00:00 2001 From: "Mohit P. Tahiliani" Date: Wed, 26 May 2021 00:19:23 +0530 Subject: [PATCH] tcp: Documentation and minor fixes for BBR --- examples/tcp/tcp-bbr-example.cc | 59 +++++---- .../doc/figures/ns3-bbr-vs-linux-bbr.png | Bin 0 -> 39331 bytes src/internet/doc/tcp.rst | 115 +++++++++++++++++- src/internet/model/tcp-bbr.cc | 16 ++- src/internet/model/tcp-bbr.h | 12 +- src/internet/model/tcp-socket-state.h | 2 +- src/internet/test/tcp-bbr-test.cc | 68 ++++++----- 7 files changed, 196 insertions(+), 76 deletions(-) create mode 100644 src/internet/doc/figures/ns3-bbr-vs-linux-bbr.png diff --git a/examples/tcp/tcp-bbr-example.cc b/examples/tcp/tcp-bbr-example.cc index 4a78464f6..ade6f88df 100644 --- a/examples/tcp/tcp-bbr-example.cc +++ b/examples/tcp/tcp-bbr-example.cc @@ -31,7 +31,8 @@ // // This program runs by default for 100 seconds and creates a new directory // called 'bbr-results' in the ns-3 root directory. The program creates one -// sub-directory in 'bbr-results' directory and three .plotme files. +// sub-directory called 'pcap' in 'bbr-results' directory (if pcap generation +// is enabled) and three .dat files. // // (1) 'pcap' sub-directory contains six PCAP files: // * bbr-0-0.pcap for the interface on Sender @@ -40,9 +41,17 @@ // * bbr-2-1.pcap for the second interface on R1 // * bbr-3-0.pcap for the first interface on R2 // * bbr-3-1.pcap for the second interface on R2 -// (2) cwnd.plotme contains congestion window trace for the sender node -// (3) throughput.plotme contains sender side throughput trace -// (4) queueSize.plotme contains queue length trace from the bottleneck link +// (2) cwnd.dat file contains congestion window trace for the sender node +// (3) throughput.dat file contains sender side throughput trace +// (4) queueSize.dat file contains queue length trace from the bottleneck link +// +// BBR algorithm enters PROBE_RTT phase in every 10 seconds. The congestion +// window is fixed to 4 segments in this phase with a goal to achieve a better +// estimate of minimum RTT (because queue at the bottleneck link tends to drain +// when the congestion window is reduced to 4 segments). +// +// The congestion window and queue occupancy traces output by this program show +// periodic drops every 10 seconds when BBR algorithm is in PROBE_RTT phase. #include "ns3/core-module.h" #include "ns3/network-module.h" @@ -54,13 +63,9 @@ using namespace ns3; -Ptr uv = CreateObject (); -double stopTime = 100; - std::string dir; -std::string str; uint32_t prev = 0; -double prevtime = 0; +Time prevTime = Seconds (0); // Calculate throughput static void @@ -68,10 +73,10 @@ TraceThroughput (Ptr monitor) { FlowMonitor::FlowStatsContainer stats = monitor->GetFlowStats (); auto itr = stats.begin (); - double curtime = Simulator::Now ().GetSeconds (); - std::ofstream thr (dir + "/throughput.plotme", std::ios::out | std::ios::app); - thr << curtime << " " << 8 * (itr->second.txBytes - prev) / (1000 * 1000 * (curtime - prevtime)) << std::endl; - prevtime = curtime; + Time curTime = Now (); + std::ofstream thr (dir + "/throughput.dat", std::ios::out | std::ios::app); + thr << curTime << " " << 8 * (itr->second.txBytes - prev) / (1000 * 1000 * (curTime.GetSeconds () - prevTime.GetSeconds ())) << std::endl; + prevTime = curTime; prev = itr->second.txBytes; Simulator::Schedule (Seconds (0.2), &TraceThroughput, monitor); } @@ -81,7 +86,7 @@ void CheckQueueSize (Ptr qd) { uint32_t qsize = qd->GetCurrentSize ().GetValue (); Simulator::Schedule (Seconds (0.2), &CheckQueueSize, qd); - std::ofstream q (dir + "/queueSize.plotme", std::ios::out | std::ios::app); + std::ofstream q (dir + "/queueSize.dat", std::ios::out | std::ios::app); q << Simulator::Now ().GetSeconds () << " " << qsize << std::endl; q.close (); } @@ -95,7 +100,7 @@ static void CwndTracer (Ptr stream, uint32_t oldval, uint32 void TraceCwnd (uint32_t nodeId, uint32_t socketId) { AsciiTraceHelper ascii; - Ptr stream = ascii.CreateFileStream (dir + "/cwnd.plotme"); + Ptr stream = ascii.CreateFileStream (dir + "/cwnd.dat"); Config::ConnectWithoutContext ("/NodeList/" + std::to_string (nodeId) + "/$ns3::TcpL4Protocol/SocketList/" + std::to_string (socketId) + "/CongestionWindow", MakeBoundCallback (&CwndTracer, stream)); } @@ -110,21 +115,20 @@ int main (int argc, char *argv []) strftime (buffer, sizeof (buffer), "%d-%m-%Y-%I-%M-%S", timeinfo); std::string currentTime (buffer); - uint32_t stream = 1; std::string tcpTypeId = "TcpBbr"; std::string queueDisc = "FifoQueueDisc"; uint32_t delAckCount = 2; bool bql = true; + bool enablePcap = false; + Time stopTime = Seconds (100); CommandLine cmd (__FILE__); - cmd.AddValue ("stream", "Seed value for random variable", stream); cmd.AddValue ("tcpTypeId", "Transport protocol to use: TcpNewReno, TcpBbr", tcpTypeId); cmd.AddValue ("delAckCount", "Delayed ACK count", delAckCount); + cmd.AddValue ("enablePcap", "Enable/Disable pcap file generation", enablePcap); cmd.AddValue ("stopTime", "Stop time for applications / simulation time will be stopTime + 1", stopTime); cmd.Parse (argc, argv); - uv->SetStream (stream); - queueDisc = std::string ("ns3::") + queueDisc; Config::SetDefault ("ns3::TcpL4Protocol::SocketType", StringValue ("ns3::" + tcpTypeId)); @@ -198,19 +202,18 @@ int main (int argc, char *argv []) ApplicationContainer sourceApps = source.Install (sender.Get (0)); sourceApps.Start (Seconds (0.0)); Simulator::Schedule (Seconds (0.2), &TraceCwnd, 0, 0); - sourceApps.Stop (Seconds (stopTime)); + 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 (Seconds (stopTime)); + sinkApps.Stop (stopTime); // Create a new directory to store the output of the program dir = "bbr-results/" + currentTime + "/"; std::string dirToSave = "mkdir -p " + dir; system (dirToSave.c_str ()); - system ((dirToSave + "/pcap/").c_str ()); // The plotting scripts are provided in the following repository, if needed: // https://github.com/mohittahiliani/BBR-Validation/ @@ -219,7 +222,7 @@ int main (int argc, char *argv []) // from the link given above and place it in the ns-3 root directory. // Uncomment the following three lines to generate plots for Congestion // Window, sender side throughput and queue occupancy on the bottleneck link. - // + // // system (("cp -R PlotScripts/gnuplotScriptCwnd " + dir).c_str ()); // system (("cp -R PlotScripts/gnuplotScriptThroughput " + dir).c_str ()); // system (("cp -R PlotScripts/gnuplotScriptQueueSize " + dir).c_str ()); @@ -230,15 +233,19 @@ int main (int argc, char *argv []) qd = tch.Install (routers.Get (0)->GetDevice (1)); Simulator::ScheduleNow (&CheckQueueSize, qd.Get (0)); - // Enable PCAP traces - bottleneckLink.EnablePcapAll (dir + "/pcap/bbr", true); + // Generate PCAP traces if it is enabled + if (enablePcap) + { + system ((dirToSave + "/pcap/").c_str ()); + bottleneckLink.EnablePcapAll (dir + "/pcap/bbr", true); + } // Check for dropped packets using Flow Monitor FlowMonitorHelper flowmon; Ptr monitor = flowmon.InstallAll (); Simulator::Schedule (Seconds (0 + 0.000001), &TraceThroughput, monitor); - Simulator::Stop (Seconds (stopTime)); + Simulator::Stop (stopTime + TimeStep (1)); Simulator::Run (); Simulator::Destroy (); diff --git a/src/internet/doc/figures/ns3-bbr-vs-linux-bbr.png b/src/internet/doc/figures/ns3-bbr-vs-linux-bbr.png new file mode 100644 index 0000000000000000000000000000000000000000..ecca58ed898b8c67109ae46e55c5593633ec49aa GIT binary patch literal 39331 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%0A^bRD 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 literal 0 HcmV?d00001 diff --git a/src/internet/doc/tcp.rst b/src/internet/doc/tcp.rst index e43cc3808..fb89f79df 100644 --- a/src/internet/doc/tcp.rst +++ b/src/internet/doc/tcp.rst @@ -51,10 +51,11 @@ connection setup and close logic. Several congestion control algorithms are supported, with CUBIC the default, and NewReno, Westwood, Hybla, HighSpeed, Vegas, Scalable, Veno, Binary Increase Congestion Control (BIC), Yet Another HighSpeed TCP (YeAH), Illinois, H-TCP, Low Extra Delay Background Transport -(LEDBAT), TCP Low Priority (TCP-LP) and and Data Center TCP (DCTCP) also supported. The model also supports -Selective Acknowledgements (SACK), Proportional Rate Reduction (PRR) and -Explicit Congestion Notification (ECN). Multipath-TCP is not yet supported in -the |ns3| releases. +(LEDBAT), TCP Low Priority (TCP-LP), Data Center TCP (DCTCP) and Bottleneck +Bandwidth and RTT (BBR) also supported. The model also supports Selective +Acknowledgements (SACK), Proportional Rate Reduction (PRR) and Explicit +Congestion Notification (ECN). Multipath-TCP is not yet supported in the |ns3| +releases. Model history +++++++++++++ @@ -1040,6 +1041,85 @@ environment. Some differences were noted: More information about DCTCP is available in the RFC 8257: https://tools.ietf.org/html/rfc8257 +BBR +^^^ +BBR (class :cpp:class:`TcpBbr`) is a congestion control algorithm that +regulates the sending rate by deriving an estimate of the bottleneck's +available bandwidth and RTT of the path. It seeks to operate at an optimal +point where sender experiences maximum delivery rate with minimum RTT. It +creates a network model comprising maximum delivery rate with minimum RTT +observed so far, and then estimates BDP (maximum bandwidth * minimum RTT) +to control the maximum amount of inflight data. BBR controls congestion by +limiting the rate at which packets are sent. It caps the cwnd to one BDP +and paces out packets at a rate which is adjusted based on the latest estimate +of delivery rate. BBR algorithm is agnostic to packet losses and ECN marks. + +pacing_gain controls the rate of sending data and cwnd_gain controls the amount +of data to send. + +The following is a high level overview of BBR congestion control algorithm: + +On receiving an ACK: + rtt = now - packet.sent_time + update_minimum_rtt (rtt) + delivery_rate = estimate_delivery_rate (packet) + update_maximum_bandwidth (delivery_rate) + +After transmitting a data packet: + bdp = max_bandwidth * min_rtt + if (cwnd * bdp < inflight) + return + if (now > nextSendTime) + { + transmit (packet) + nextSendTime = now + packet.size / (pacing_gain * max_bandwidth) + } + else + return + Schedule (nextSendTime, Send) + +To enable BBR on all TCP sockets, the following configuration can be used: + +:: + + Config::SetDefault ("ns3::TcpL4Protocol::SocketType", TypeIdValue (TcpBbr::GetTypeId ())); + +To enable BBR on a chosen TCP socket, the following configuration can be used +(note that an appropriate Node ID must be used instead of 1): + +:: + + Config::Set ("$ns3::NodeListPriv/NodeList/1/$ns3::TcpL4Protocol/SocketType", TypeIdValue (TcpBbr::GetTypeId ())); + +The ns-3 implementation of BBR is based on its Linux implementation. Linux 5.4 +kernel implementation has been used to validate the behavior of ns-3 +implementation of BBR (See below section on Validation). + +In addition, the following unit tests have been written to validate the +implementation of BBR in ns-3: + +* BBR should enable (if not already done) TCP pacing feature. +* Test to validate the values of pacing_gain and cwnd_gain in different phases +of BBR. + +An example program, examples/tcp/tcp-bbr-example.cc, is provided to experiment +with BBR for one long running flow. This example uses a simple topology +consisting of one sender, one receiver and two routers to examine congestion +window, throughput and queue control. A program similar to this has been run +using the Network Stack Tester (NeST) using BBR from Linux kernel 5.4, and the +results were compared against ns-3 results. + +More information about BBR is available in the following Internet Draft: +https://tools.ietf.org/html/draft-cardwell-iccrg-bbr-congestion-control-00 + +More information about Delivery Rate Estimation is in the following draft: +https://tools.ietf.org/html/draft-cheng-iccrg-delivery-rate-estimation-00 + +For an academic peer-reviewed paper on the BBR implementation in ns-3, +please refer to: + +* Vivek Jain, Viyom Mittal and Mohit P. Tahiliani. "Design and Implementation of TCP BBR in ns-3." In Proceedings of the 10th Workshop on ns-3, pp. 16-22. 2018. (https://dl.acm.org/doi/abs/10.1145/3199902.3199911) + Support for Explicit Congestion Notification (ECN) ++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -1312,6 +1392,7 @@ section below on :ref:`Writing-tcp-tests`. * **tcp-ledbat-test:** Unit tests on the LEDBAT congestion control * **tcp-lp-test:** Unit tests on the TCP-LP congestion control * **tcp-dctcp-test:** Unit tests on the DCTCP congestion control +* **tcp-bbr-test:** Unit tests on the BBR congestion control * **tcp-option:** Unit tests on TCP options * **tcp-pkts-acked-test:** Unit test the number of time that PktsAcked is called * **tcp-rto-test:** Unit test behavior after a RTO occurs @@ -1439,6 +1520,32 @@ a similar scenario but with ECN enabled throughout. TCP ECN operation is tested in the ARED and RED tests that are documented in the traffic-control module documentation. +Like DCTCP and TCP CUBIC, the ns-3 implementation of TCP BBR was validated +against the BBR implementation of Linux kernel 5.4 using Network Stack Tester +(NeST). NeST is a python package which allows the users to emulate kernel space +protocols using Linux network namespaces. Figure :ref:`fig-ns3-bbr-vs-linux-bbr` +compares the congestion window evolution between ns-3 and Linux for a single +flow operating over a 10 Mbps link with 10 ms base RTT and FIFO queue +discipline. + +.. _fig-ns3-bbr-vs-linux-bbr: + +.. figure:: figures/ns3-bbr-vs-linux-bbr.* + :scale: 80 % + :align: center + + Congestion window evolution: ns-3 BBR vs. Linux BBR (using NeST) + +It can be observed that the congestion window traces for ns-3 BBR closely +overlap with Linux BBR. The periodic drops in congestion window every 10 +seconds depict the PROBE_RTT phase of the BBR algorithm. In this phase, BBR +algorithm keeps the congestion window fixed to 4 segments. + +The example program, examples/tcp-bbr-example.cc has been used to obtain the +congestion window curve shown in Figure :ref:`fig-ns3-bbr-vs-linux-bbr`. The +detailed instructions to reproduce ns-3 plot and NeST plot can be found at: +https://github.com/mohittahiliani/BBR-Validation + Writing a new congestion control algorithm ++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/internet/model/tcp-bbr.cc b/src/internet/model/tcp-bbr.cc index 726a587e9..edaca04a0 100644 --- a/src/internet/model/tcp-bbr.cc +++ b/src/internet/model/tcp-bbr.cc @@ -39,6 +39,11 @@ TcpBbr::GetTypeId (void) .SetParent () .AddConstructor () .SetGroupName ("Internet") + .AddAttribute ("Stream", + "Random number stream (default is set to 4 to align with Linux results)", + UintegerValue (4), + MakeUintegerAccessor (&TcpBbr::SetStream), + MakeUintegerChecker ()) .AddAttribute ("HighGain", "Value of high gain", DoubleValue (2.89), @@ -50,12 +55,12 @@ TcpBbr::GetTypeId (void) MakeUintegerAccessor (&TcpBbr::m_bandwidthWindowLength), MakeUintegerChecker ()) .AddAttribute ("RttWindowLength", - "Length of bandwidth windowed filter", + "Length of RTT windowed filter", TimeValue (Seconds (10)), MakeTimeAccessor (&TcpBbr::m_rtPropFilterLen), MakeTimeChecker ()) .AddAttribute ("ProbeRttDuration", - "Length of bandwidth windowed filter", + "Time to be spent in PROBE_RTT phase", TimeValue (MilliSeconds (200)), MakeTimeAccessor (&TcpBbr::m_probeRttDuration), MakeTimeChecker ()) @@ -109,6 +114,7 @@ TcpBbr::TcpBbr (const TcpBbr &sock) m_rtPropFilterLen (sock.m_rtPropFilterLen), m_rtPropStamp (sock.m_rtPropStamp), m_isInitialized (sock.m_isInitialized), + m_uv (sock.m_uv), m_delivered (sock.m_delivered), m_appLimited (sock.m_appLimited), m_txItemDelivered (sock.m_txItemDelivered), @@ -122,15 +128,13 @@ TcpBbr::TcpBbr (const TcpBbr &sock) m_hasSeenRtt (sock.m_hasSeenRtt) { NS_LOG_FUNCTION (this); - m_uv = CreateObject (); } -int64_t -TcpBbr::AssignStreams (int64_t stream) +void +TcpBbr::SetStream (uint32_t stream) { NS_LOG_FUNCTION (this << stream); m_uv->SetStream (stream); - return 1; } void diff --git a/src/internet/model/tcp-bbr.h b/src/internet/model/tcp-bbr.h index 675c2262d..61a1dea48 100644 --- a/src/internet/model/tcp-bbr.h +++ b/src/internet/model/tcp-bbr.h @@ -80,13 +80,11 @@ public: /** * Assign a fixed random variable stream number to the random variables - * used by this model. Return the number of streams (possibly zero) that - * have been assigned. + * used by this model. * * \param stream first stream index to use - * \return the number of stream indices assigned by this model */ - virtual int64_t AssignStreams (int64_t stream); + virtual void SetStream (uint32_t stream); virtual std::string GetName () const; virtual bool HasCongControl () const; @@ -353,7 +351,7 @@ private: Time m_probeRtPropStamp {Seconds (0)}; //!< The wall clock time at which the current BBR.RTProp sample was obtained. Time m_probeRttDoneStamp {Seconds (0)}; //!< Time to exit from BBR_PROBE_RTT state bool m_probeRttRoundDone {false}; //!< True when it is time to exit BBR_PROBE_RTT - bool m_packetConservation {false}; //!< + bool m_packetConservation {false}; //!< Enable/Disable packet conservation mode uint32_t m_priorCwnd {0}; //!< The last-known good congestion window bool m_idleRestart {false}; //!< When restarting from idle, set it true uint32_t m_targetCWnd {0}; //!< Target value for congestion window, adapted to the estimated BDP @@ -370,9 +368,9 @@ private: Ptr m_uv {nullptr}; //!< Uniform Random Variable uint64_t m_delivered {0}; //!< The total amount of data in bytes delivered so far uint32_t m_appLimited {0}; //!< The index of the last transmitted packet marked as application-limited - uint32_t m_txItemDelivered {0}; //!< + uint32_t m_txItemDelivered {0}; //!< The number of bytes already delivered at the time of new packet transmission uint32_t m_extraAckedGain {1}; //!< Gain factor for adding extra ack to cwnd - uint32_t m_extraAcked[2] {0, 0}; //!< Maximum excess data acked in epoch + uint32_t m_extraAcked [2] {0, 0}; //!< Maximum excess data acked in epoch uint32_t m_extraAckedWinRtt {0}; //!< Age of extra acked in rtt uint32_t m_extraAckedWinRttLength {5}; //!< Window length of extra acked window uint32_t m_ackEpochAckedResetThresh {1 << 17}; //!< Max allowed val for m_ackEpochAcked, after which sampling epoch is reset diff --git a/src/internet/model/tcp-socket-state.h b/src/internet/model/tcp-socket-state.h index bc07d639b..22069804a 100644 --- a/src/internet/model/tcp-socket-state.h +++ b/src/internet/model/tcp-socket-state.h @@ -207,7 +207,7 @@ public: EcnCodePoint_t m_ectCodePoint {Ect0}; //!< ECT code point to use - uint32_t m_lastAckedSackedBytes {0}; //!< Last acked and sacked recorded upon receiving last acknowledgment + uint32_t m_lastAckedSackedBytes {0}; //!< The number of bytes acked and sacked as indicated by the current ACK received. This is similar to acked_sacked variable in Linux /** * \brief Get cwnd in segments rather than bytes diff --git a/src/internet/test/tcp-bbr-test.cc b/src/internet/test/tcp-bbr-test.cc index 885e1c0d5..98547f0c5 100644 --- a/src/internet/test/tcp-bbr-test.cc +++ b/src/internet/test/tcp-bbr-test.cc @@ -48,8 +48,7 @@ private: TcpBbrPacingEnableTest::TcpBbrPacingEnableTest (bool pacing, const std::string &name) : TestCase (name), m_pacing (pacing) -{ -} +{} void TcpBbrPacingEnableTest::DoRun () @@ -93,8 +92,7 @@ TcpBbrCheckGainValuesTest::TcpBbrCheckGainValuesTest (TcpBbr::BbrMode_t state, : TestCase (name), m_mode (state), m_highGain (highGain) -{ -} +{} void TcpBbrCheckGainValuesTest::DoRun () @@ -113,34 +111,40 @@ TcpBbrCheckGainValuesTest::ExecuteTest () TcpBbr::BbrMode_t desiredMode; switch (m_mode) { - case TcpBbr::BBR_STARTUP: - cong->EnterStartup (); - desiredPacingGain = m_highGain; - desiredCwndGain = m_highGain; - actualPacingGain = cong->GetPacingGain (); - actualCwndGain = cong->GetCwndGain (); - desiredMode = TcpBbr::BBR_STARTUP; - break; - case TcpBbr::BBR_DRAIN: - cong->EnterDrain (); - desiredPacingGain = 1 / m_highGain; - desiredCwndGain = m_highGain; - desiredMode = TcpBbr::BBR_DRAIN; - break; - case TcpBbr::BBR_PROBE_BW: - cong->EnterProbeBW (); - desiredPacingGain = 1; - desiredCwndGain = 2; - desiredMode = TcpBbr::BBR_PROBE_BW; - break; - case TcpBbr::BBR_PROBE_RTT: - cong->EnterProbeRTT (); - desiredPacingGain = 1; - desiredCwndGain = 1; - desiredMode = TcpBbr::BBR_PROBE_RTT; - break; - default: - NS_ASSERT (false); + case TcpBbr::BBR_STARTUP: + cong->EnterStartup (); + desiredPacingGain = m_highGain; + desiredCwndGain = m_highGain; + actualPacingGain = cong->GetPacingGain (); + actualCwndGain = cong->GetCwndGain (); + desiredMode = TcpBbr::BBR_STARTUP; + break; + case TcpBbr::BBR_DRAIN: + cong->EnterDrain (); + desiredPacingGain = 1 / m_highGain; + desiredCwndGain = m_highGain; + desiredMode = TcpBbr::BBR_DRAIN; + break; + case TcpBbr::BBR_PROBE_BW: + cong->EnterProbeBW (); + // The value of desiredPacingGain is sensitive to the setting of random + // variable stream. The value of 1.25 has been used in this test with a + // stream value of 4 (default for TCP BBR). Note that if the stream value + // is changed, this test might fail because when BBR enters the PROBE_BW + // phase, the value of actualPacingGain is chosen randomly from 1.25, + // 0.75, 1, 1, 1, 1, 1, 1. + desiredPacingGain = 1.25; + desiredCwndGain = 2; + desiredMode = TcpBbr::BBR_PROBE_BW; + break; + case TcpBbr::BBR_PROBE_RTT: + cong->EnterProbeRTT (); + desiredPacingGain = 1; + desiredCwndGain = 1; + desiredMode = TcpBbr::BBR_PROBE_RTT; + break; + default: + NS_ASSERT (false); } actualPacingGain = cong->GetPacingGain ();