Raising the UDP rate until it fills the whole bridge starves TCP completely. The plot of congestion window against time, annotated with both UDP rates, is the deliverable.
Objectives
Do not copy. Read for understanding and the viva- Complete questions 23 to 24 of the manual: saturating the bottleneck and plotting cwnd
- Prepare the deliverable before the lab and finish it during the session
- Be ready to explain every step in the viva
Questions Covered
Do not copy. Read for understanding and the viva| Question | Requirement | Status |
|---|---|---|
| Q23 | In the last session 8, Increase the UDP rate at 30 second to Rate2 such that it clogs… | Complete |
| Q24 | Use MatPlotlLib or GNUPlot to visualize cwnd vs time, also mention Rate1 and Rate2 | Complete |
Preparation
Do not copy. Read for understanding and the viva- Schedule the rate change with
Simulator::Schedule(Seconds(30.0), ...)that sets the OnOff application’s DataRate attribute to Rate2. - Plot with matplotlib: time on x, cwnd in bytes on y, vertical lines at 20 s and 30 s labelled Rate1 and Rate2.
- Explain each phase of the curve in the record: slow start, steady state, halving after 20 s, collapse after 30 s.
Question 23
Problem Statement
Write in lab recordIn the last session 8, Increase the UDP rate at 30 second to Rate2 such that it clogs whole of the dumbbell bridge capacity.
Solution
Write in lab recorddumbbell-rate2.cc is the Session 8 script with one scheduled event added and the run extended to 45 s. Bridge 1 Mbit/s, Rate1 500 kbit/s from 20 s, Rate2 1 Mbit/s from 30 s.
Steps
- Copy the listing into
scratch/dumbbell-rate2.ccand run./ns3 run scratch/dumbbell-rate2. - The UDP
OnOffApplicationis installed once withDataRateRate1. ItsDataRateattribute can be changed while it runs; the application reads it every time it schedules the next packet. ChangeRate(Ptr<Application> app, DataRate rate)callsapp->SetAttribute("DataRate", DataRateValue(rate))and prints the time. It is scheduled withSimulator::Schedule(Seconds(30.0), &ChangeRate, udpSrc.Get(0), DataRate("1Mbps")).- Rate1, Rate2 and both times are command-line options (
--rate1,--rate2,--udpStart,--rate2Time), so the same binary can show the examiner other cases. - The cwnd trace is identical to Session 8: connect at 1.001 s to
/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindowand writetime cwndtocwnd.txt.
Program
/*
* dumbbell-rate2.cc -- MCSL-223 Session 9, questions 23 and 24
*
* Purpose of the program:
* The Session 8 dumbbell, extended: TCP starts at 1 s, UDP starts at 20 s
* at Rate1 (500 kb/s, half the 1 Mbit/s bridge) and at 30 s the UDP rate
* is raised to Rate2 (1 Mbit/s, the whole bridge) by a scheduled event
* that changes the OnOff application's DataRate attribute (Q23). The
* congestion window of the TCP socket is traced to cwnd.txt and plotted
* with plot_cwnd.py, which marks Rate1 and Rate2 (Q24).
*
* Topology (Session 2 dumbbell, all links point-to-point):
* n0 (TCP src) --10Mbps,1ms--\ /--10Mbps,1ms-- n4 (TCP sink :8080)
* n2 --1Mbps,10ms-- n3
* n1 (UDP src) --10Mbps,1ms--/ (the bridge) \--10Mbps,1ms-- n5 (UDP sink :9000)
*
* Build and run (ns-3.36 or later):
* cp dumbbell-rate2.cc scratch/
* ./ns3 run scratch/dumbbell-rate2
* python3 plot_cwnd.py cwnd.txt cwnd.png
*/
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("DumbbellRate2");
/*
* CwndChange: trace sink for the CongestionWindow attribute of the TCP
* socket. Writes "time newCwnd" (bytes) on every change.
*/
static void
CwndChange(Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, uint32_t newCwnd)
{
(void)oldCwnd;
*stream->GetStream() << Simulator::Now().GetSeconds() << " " << newCwnd << std::endl;
}
/*
* TraceCwnd: connects CwndChange to the first TCP socket of node 0.
* Scheduled at 1.001 s, just after BulkSend has created the socket.
*/
static void
TraceCwnd(Ptr<OutputStreamWrapper> stream)
{
Config::ConnectWithoutContext("/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow",
MakeBoundCallback(&CwndChange, stream));
}
/*
* ChangeRate: scheduled at 30 s. Sets the DataRate attribute of the
* running OnOff application; the next packet interval uses the new rate.
*/
static void
ChangeRate(Ptr<Application> app, DataRate rate)
{
app->SetAttribute("DataRate", DataRateValue(rate));
std::cout << Simulator::Now().GetSeconds() << " s: UDP rate changed to " << rate
<< std::endl;
}
/*
* main: builds the dumbbell, installs the flows, schedules the rate change
* and the cwnd tracer, and prints the totals at the end.
*/
int
main(int argc, char* argv[])
{
std::string bridgeRate = "1Mbps";
std::string rate1 = "500kb/s"; // half of the bridge
std::string rate2 = "1Mbps"; // whole bridge
double udpStart = 20.0;
double rate2Time = 30.0;
double simTime = 45.0;
CommandLine cmd(__FILE__);
cmd.AddValue("bridgeRate", "Data rate of the n2-n3 bridge", bridgeRate);
cmd.AddValue("rate1", "UDP rate from udpStart (Rate1)", rate1);
cmd.AddValue("rate2", "UDP rate from rate2Time (Rate2)", rate2);
cmd.AddValue("udpStart", "Time in seconds at which UDP starts", udpStart);
cmd.AddValue("rate2Time", "Time in seconds at which UDP switches to Rate2", rate2Time);
cmd.AddValue("simTime", "Simulation time in seconds", simTime);
cmd.Parse(argc, argv);
Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(1000));
// NewReno halves cwnd on loss, as in the formula sheet (ns-3.35+ defaults to Cubic)
Config::SetDefault("ns3::TcpL4Protocol::SocketType", TypeIdValue(TcpNewReno::GetTypeId()));
// ---- nodes: left first so the TCP source is NodeList/0 ---------------
NodeContainer left;
left.Create(2); // n0, n1
NodeContainer routers;
routers.Create(2); // n2, n3
NodeContainer right;
right.Create(2); // n4, n5
PointToPointHelper access;
access.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
access.SetChannelAttribute("Delay", StringValue("1ms"));
PointToPointHelper bridge;
bridge.SetDeviceAttribute("DataRate", StringValue(bridgeRate));
bridge.SetChannelAttribute("Delay", StringValue("10ms"));
NetDeviceContainer d02 = access.Install(left.Get(0), routers.Get(0));
NetDeviceContainer d12 = access.Install(left.Get(1), routers.Get(0));
NetDeviceContainer d23 = bridge.Install(routers.Get(0), routers.Get(1));
NetDeviceContainer d34 = access.Install(routers.Get(1), right.Get(0));
NetDeviceContainer d35 = access.Install(routers.Get(1), right.Get(1));
InternetStackHelper stack;
stack.Install(left);
stack.Install(routers);
stack.Install(right);
Ipv4AddressHelper address;
address.SetBase("10.1.1.0", "255.255.255.0");
address.Assign(d02);
address.SetBase("10.1.2.0", "255.255.255.0");
address.Assign(d12);
address.SetBase("10.1.3.0", "255.255.255.0");
address.Assign(d23);
address.SetBase("10.1.4.0", "255.255.255.0");
Ipv4InterfaceContainer i34 = address.Assign(d34);
address.SetBase("10.1.5.0", "255.255.255.0");
Ipv4InterfaceContainer i35 = address.Assign(d35);
Ipv4GlobalRoutingHelper::PopulateRoutingTables();
// ---- TCP n0 -> n4 from 1 s ------------------------------------------
uint16_t tcpPort = 8080;
PacketSinkHelper tcpSinkHelper("ns3::TcpSocketFactory",
InetSocketAddress(Ipv4Address::GetAny(), tcpPort));
ApplicationContainer tcpSink = tcpSinkHelper.Install(right.Get(0));
tcpSink.Start(Seconds(0.0));
tcpSink.Stop(Seconds(simTime));
BulkSendHelper bulk("ns3::TcpSocketFactory", InetSocketAddress(i34.GetAddress(1), tcpPort));
bulk.SetAttribute("MaxBytes", UintegerValue(0));
ApplicationContainer tcpSrc = bulk.Install(left.Get(0));
tcpSrc.Start(Seconds(1.0));
tcpSrc.Stop(Seconds(simTime));
// ---- UDP n1 -> n5 at Rate1 from 20 s --------------------------------
uint16_t udpPort = 9000;
PacketSinkHelper udpSinkHelper("ns3::UdpSocketFactory",
InetSocketAddress(Ipv4Address::GetAny(), udpPort));
ApplicationContainer udpSink = udpSinkHelper.Install(right.Get(1));
udpSink.Start(Seconds(0.0));
udpSink.Stop(Seconds(simTime));
OnOffHelper onoff("ns3::UdpSocketFactory", InetSocketAddress(i35.GetAddress(1), udpPort));
onoff.SetConstantRate(DataRate(rate1), 1000);
ApplicationContainer udpSrc = onoff.Install(left.Get(1));
udpSrc.Start(Seconds(udpStart));
udpSrc.Stop(Seconds(simTime));
// ---- Q23: switch the same OnOff application to Rate2 at 30 s ---------
Simulator::Schedule(Seconds(rate2Time), &ChangeRate, udpSrc.Get(0), DataRate(rate2));
// ---- Q24: cwnd trace, connected just after the socket exists ---------
AsciiTraceHelper ascii;
Simulator::Schedule(Seconds(1.001), &TraceCwnd, ascii.CreateFileStream("cwnd.txt"));
Simulator::Stop(Seconds(simTime));
Simulator::Run();
// ---- report ---------------------------------------------------------
Ptr<PacketSink> ts = DynamicCast<PacketSink>(tcpSink.Get(0));
Ptr<PacketSink> us = DynamicCast<PacketSink>(udpSink.Get(0));
std::cout << "TCP sink n4: " << ts->GetTotalRx() << " bytes over " << simTime - 1.0
<< " s = " << ts->GetTotalRx() * 8.0 / (simTime - 1.0) / 1000.0 << " kbit/s"
<< std::endl;
std::cout << "UDP sink n5: " << us->GetTotalRx() << " bytes over " << simTime - udpStart
<< " s = " << us->GetTotalRx() * 8.0 / (simTime - udpStart) / 1000.0 << " kbit/s"
<< std::endl;
Simulator::Destroy();
return 0;
}Configuration
The defaults reproduce the question. These runs are worth keeping in the record because the examiner can ask what happens if Rate2 is not the whole bridge:
| Command | Rate2 on the wire | Expected cwnd after 30 s |
|---|---|---|
./ns3 run scratch/dumbbell-rate2 | 1.03 Mbit/s, above the bridge | resets to 1 kB, stays there |
./ns3 run "scratch/dumbbell-rate2 --rate2=750kb/s" | 0.77 Mbit/s | sawtooth between about 10 kB and 20 kB, TCP keeps about 230 kbit/s |
./ns3 run "scratch/dumbbell-rate2 --rate2=500kb/s" | same as Rate1 | no change at 30 s; the plot shows only the 20 s step |
./ns3 run "scratch/dumbbell-rate2 --rate2Time=25 --simTime=40" | 1.03 Mbit/s from 25 s | collapse five seconds earlier; move the second marker in the plot script |
Diagram
n0 (TCP BulkSend from 1 s) --10 Mbps, 1 ms--\ /--10 Mbps, 1 ms-- n4 (TCP sink :8080)
n2 ---- 1 Mbps, 10 ms bridge ---- n3
n1 (UDP OnOff: Rate1 at 20 s, --10 Mbps, 1 ms--/ DropTail 100 packets \--10 Mbps, 1 ms-- n5 (UDP sink :9000)
Rate2 at 30 s)Output
Expected output (ns-3 is not installed here; the listing was checked by reading against the ns-3.36 helper API):
$ ./ns3 run scratch/dumbbell-rate2
30 s: UDP rate changed to 1000000bps
TCP sink n4: 2880000 bytes over 44 s = 523.636 kbit/s
UDP sink n5: 2455000 bytes over 25 s = 785.6 kbit/sExpected cwnd.txt around the change:
$ awk '$1>29.5 && $1<34' cwnd.txt
29.6104 44000
29.6112 22000
30.4257 23000
30.9917 1000
31.9917 2000
32.4133 1000
34.4133 1000Explanation
At Rate2 the UDP source offers 1000-byte datagrams every 8 ms. On the wire each is 1030 bytes, so the offered load is
slightly more than the bridge can carry even with no TCP at all. The queue at n2 is now permanently full of UDP datagrams. A TCP segment is accepted only if it arrives in the short gap after a departure and before the next UDP arrival, so almost every TCP segment is tail-dropped, and even the UDP flow loses about 3 percent (979 of 1000 kbit/s delivered). TCP sees repeated losses without enough duplicate ACKs for fast retransmit, so it falls back to retransmission timeouts: cwnd is reset to one segment, the timeout doubles after each failure, and the flow delivers almost nothing. Between 30 s and 45 s the TCP sink gains only about 30 kB, against 2.27 MB in the first 19 s and 580 kB during the Rate1 phase. The UDP average printed over 25 s mixes the two phases: 10 s at about 495 kbit/s and 15 s at about 979 kbit/s.
Question 24
Problem Statement
Write in lab recordUse MatPlotlLib or GNUPlot to visualize cwnd vs time, also mention Rate1 and Rate2.
Solution
Write in lab recordSteps
- Install matplotlib once:
pip install matplotlib. - Run
python3 plot_cwnd.py cwnd.txt cwnd.pngin the ns-3 directory after the simulation. The script reads the two-column trace, draws the window as a step plot and adds two dashed vertical lines at 20 s and 30 s labelled Rate1 and Rate2. - For gnuplot instead:
gnuplot plot_cwnd.gp. It useswith steps, twoset arrow ... noheadlines at 20 and 30 andset labelfor the rates; the output file is the samecwnd.png. - Paste
cwnd.pngin the record with the phase table below written under it.
Program
Lab record: every tab is one file of the answer. Write all of them.
#!/usr/bin/env python3
"""plot_cwnd.py -- MCSL-223 Session 9, Q24.
Reads cwnd.txt ("time cwnd" per line, written by dumbbell-rate2.cc) and
draws the congestion window against time with vertical markers at the two
UDP rate changes.
Usage: python3 plot_cwnd.py [cwnd.txt] [cwnd.png]
Needs: matplotlib (pip install matplotlib)
"""
import sys
import matplotlib
matplotlib.use("Agg") # write a file, no display needed
import matplotlib.pyplot as plt # noqa: E402
RATE1_TIME, RATE1 = 20.0, "Rate1 = 500 kbit/s (half the bridge)"
RATE2_TIME, RATE2 = 30.0, "Rate2 = 1 Mbit/s (whole bridge)"
def read_trace(path):
"""Return two lists, time in seconds and cwnd in bytes."""
times, cwnds = [], []
with open(path) as f:
for line in f:
parts = line.split()
if len(parts) == 2:
times.append(float(parts[0]))
cwnds.append(int(parts[1]))
return times, cwnds
def main():
src = sys.argv[1] if len(sys.argv) > 1 else "cwnd.txt"
out = sys.argv[2] if len(sys.argv) > 2 else "cwnd.png"
times, cwnds = read_trace(src)
if not times:
sys.exit(f"{src}: no samples found")
top = max(cwnds)
plt.figure(figsize=(9, 5))
plt.step(times, cwnds, where="post", lw=1.2, label="cwnd (bytes)")
plt.axvline(RATE1_TIME, color="tab:orange", ls="--")
plt.text(RATE1_TIME + 0.3, top * 0.95, RATE1, color="tab:orange")
plt.axvline(RATE2_TIME, color="tab:red", ls="--")
plt.text(RATE2_TIME + 0.3, top * 0.85, RATE2, color="tab:red")
plt.xlabel("Time (s)")
plt.ylabel("Congestion window (bytes)")
plt.title("TCP cwnd on the 1 Mbit/s dumbbell bridge under UDP load")
plt.grid(alpha=0.3)
plt.legend(loc="upper left")
plt.tight_layout()
plt.savefig(out, dpi=120)
print(f"wrote {out}: {len(times)} samples, max cwnd {top} bytes, last {cwnds[-1]} bytes")
if __name__ == "__main__":
main()# plot_cwnd.gp -- MCSL-223 Session 9, Q24 (gnuplot alternative to plot_cwnd.py)
# Run after the simulation: gnuplot plot_cwnd.gp
# cwnd.txt columns: time (s), congestion window (bytes)
set terminal pngcairo size 900,540
set output "cwnd.png"
set title "TCP cwnd on the 1 Mbit/s dumbbell bridge under UDP load"
set xlabel "Time (s)"
set ylabel "Congestion window (bytes)"
set key left top
set grid
set arrow from 20, graph 0 to 20, graph 1 nohead dt 2 lc rgb "dark-orange"
set label "Rate1 = 500 kbit/s (half the bridge)" at 20.3, graph 0.95 tc rgb "dark-orange"
set arrow from 30, graph 0 to 30, graph 1 nohead dt 2 lc rgb "red"
set label "Rate2 = 1 Mbit/s (whole bridge)" at 30.3, graph 0.85 tc rgb "red"
plot "cwnd.txt" using 1:2 with steps lw 1.5 title "cwnd (bytes)"Output
The script was run here on a synthetic cwnd.txt with the expected shape (matplotlib 3.x, Python 3.13) to check that it draws and labels correctly:
$ python3 plot_cwnd.py cwnd.txt cwnd.png
wrote cwnd.png: 981 samples, max cwnd 105000 bytes, last 3000 bytesExpected figure: a step curve of cwnd in bytes against time from 1 s to 45 s, an orange dashed line at 20 s labelled “Rate1 = 500 kbit/s (half the bridge)” and a red dashed line at 30 s labelled “Rate2 = 1 Mbit/s (whole bridge)”.
Explanation
Phase by phase, tie each part of the curve to the formula sheet:
| Phase | Time | What the curve does | Formula-sheet rule |
|---|---|---|---|
| Slow start | 1.0 to 1.9 s | 10 kB doubling each RTT to about 110 kB | cwnd doubles every RTT |
| First loss | about 1.9 s | drop to about 55 kB | queue of 100 packets overflows; cwnd halves |
| Congestion avoidance | 2 to 20 s | slow straight climb to about 82 kB, no loss | plus one segment per RTT; RTT is about 0.5 s because of the queue |
| Rate1 | 20 to 30 s | halves at about 20.7 s, then a sawtooth between about 25 kB and 45 kB | UDP takes half the bridge and half the queue; TCP loses every few seconds and halves each time |
| Rate2 | 30 to 45 s | halves once more, then collapses to 1 kB with rare steps to 2 kB | queue is always full of UDP; every loss is a timeout, cwnd resets to one segment and the timeout doubles |
Two points to make in the viva. First, during phases 2 and 3 the TCP throughput is constant even though cwnd changes a lot, because cwnd is above the 3000-byte bandwidth-delay product; the window only decides how deep the queue is. Second, the throughput bound in the formula sheet, cwnd over RTT, explains the collapse: with cwnd at 1000 bytes and an RTT that has grown to the retransmission timeout (a second or more), the bound is under 8 kbit/s, which is what the sink sees.
Why UDP wins: the OnOff source has no feedback loop. TCP measures loss and reduces its window; UDP measures nothing and keeps its rate, so at Rate2 it takes the bridge and TCP gets only the gaps. This is the argument for congestion control, fair queueing at routers, or rate limits on UDP applications.
Formula Sheet
Do not copy. Read for understanding and the vivaThroughput
Bytes received at the sink divided by the time the flow was active, converted to bits per second:
Bandwidth-delay product
The amount of data in flight on a link of capacity and round-trip time . A TCP window smaller than this cannot fill the link:
For the Session 10 defaults, and one-way delay give and .
Transmission and propagation delay
where is packet size in bits, link rate, distance and signal speed (about in copper or fibre).
Constant bit rate traffic
An OnOff application with packet size bytes and rate bit/s sends one packet every
TCP congestion window
Slow start doubles the window every RTT until the threshold; congestion avoidance adds one segment per RTT; a loss halves it (TCP NewReno):
The maximum throughput of one TCP flow is bounded by
Packet loss
Viva Questions
Do not copy. Read for understanding and the viva- Q: How is the UDP rate changed at 30 s without a second application? A: A scheduled event calls
SetAttribute("DataRate", ...)on the runningOnOffApplication; it reads the attribute when it schedules each next packet. - Q: What are Rate1 and Rate2 and where do they come from? A: Half and all of the bridge capacity: 500 kbit/s and 1 Mbit/s for a 1 Mbit/s bridge.
- Q: Why does UDP at exactly 1 Mbit/s still overload a 1 Mbit/s link? A: The rate counts payload; UDP, IP and PPP headers add 30 bytes per 1000-byte datagram, so the wire load is 1.03 Mbit/s.
- Q: Why does cwnd go to 1000 bytes after 30 s instead of halving? A: With nearly every segment dropped there are no duplicate ACKs, so the loss is detected by timeout, which resets cwnd to one segment.
- Q: What does
plt.stepshow thatplt.plotwould not? A: cwnd changes in jumps at ACK or loss events; a step plot draws it as it is, a line plot draws false slopes. - Q: Why is the TCP throughput flat between 2 s and 20 s while cwnd climbs? A: cwnd is above the bandwidth-delay product, so the bridge is already full; extra window only fills the queue.
- Q: How would you make TCP survive Rate2? A: A queue discipline that isolates flows (fair queueing,
FqCoDelin ns-3) or a smaller shared queue with early drops (RED, CoDel) so UDP cannot monopolise the buffer. - Q: Where is the bandwidth-delay product of the bridge? A: 1 Mbit/s times 24 ms, 3000 bytes, three segments of 1000 bytes.
Common Mistakes
Do not copy. Read for understanding and the viva- Installing a second OnOff application at 30 s instead of changing the rate of the first; the two sources then overlap and the offered load is Rate1 plus Rate2.
- Passing the attribute as
StringValue("1Mbps")to a function that expectsDataRate; both work withSetAttribute, but mixing them inSimulator::Schedulearguments gives a compile error about the callback signature. - Running only to 40 s, which leaves too little of the Rate2 phase to show the collapse; use 45 s or more.
- Drawing the marker lines but not labelling them; the question explicitly asks to mention Rate1 and Rate2 on the plot.
- Describing the Rate2 phase as “cwnd halves” when it actually resets to one segment on timeout; the record should say which loss detection happened.
- Plotting cwnd in segments after tracing it in bytes without saying so; the axis label must match the trace.
Session Summary
Write in lab record- Source listing of
dumbbell-rate2.ccwith the header comment and the dumbbell diagram marking Rate1 at 20 s and Rate2 at 30 s - Run output with the rate-change line and the TCP and UDP totals
cwnd.txtexcerpt around 30 s showing the reset to 1000 bytesplot_cwnd.py(orplot_cwnd.gp) and the figurecwnd.pngwith both vertical markers labelled- The five-phase table linking each part of the curve to the formula sheet
- The wire-load calculation showing why Rate2 exceeds the bridge