aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/020-lossy-paths
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 16:57:02 +0100
committereunhhu <[email protected]>2026-09-29 16:57:02 +0100
commit1d6d2430960622589f6c11220e09ba4b6dba2989 (patch)
tree2d2611c6bde7370e64d0b444f8ef86797663387a /research/experiments/020-lossy-paths
parentc494ec7a37ff4863d420b4a9ccbe1df93e88de2d (diff)
downloadpure-1d6d2430960622589f6c11220e09ba4b6dba2989.tar.gz
pure-1d6d2430960622589f6c11220e09ba4b6dba2989.zip
Put ngtcp2 beside quinn in M6, as it is in M5
drain.c takes a rate and seconds as quinn's client does (paced messages stamped with the time they were written, and no more written past the seconds), and its service writes quinn's line for each stream: the messages read in each 100 ms and how long they took. 020's run.py runs it as a third, on port 40005 behind the same bottleneck, and --who picks whom to run.
Diffstat (limited to 'research/experiments/020-lossy-paths')
-rw-r--r--research/experiments/020-lossy-paths/run.py61
1 files changed, 40 insertions, 21 deletions
diff --git a/research/experiments/020-lossy-paths/run.py b/research/experiments/020-lossy-paths/run.py
index 9dbb53f..542360a 100644
--- a/research/experiments/020-lossy-paths/run.py
+++ b/research/experiments/020-lossy-paths/run.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""M6 of docs/design/011-pure-transport.md: a link on a path of delay, loss,
-reordering and a bottleneck, against quinn, and beside TCP CUBIC.
+reordering and a bottleneck, against quinn and ngtcp2, and beside TCP CUBIC.
Each case runs in `unshare -rn` (an unprivileged network namespace) whose
loopback is the path: MTU 1500, and TCP's segments no larger
@@ -8,15 +8,15 @@ loopback is the path: MTU 1500, and TCP's segments no larger
RTT), jitter (packets reorder) and random loss; the way to the receivers
(ports 40000-40007) then goes through a 50 Mbit/s bottleneck (tbf) whose
queue holds a round trip of it, and the way back (answers, TCP's
-acknowledgments) does not queue behind it. Pure and quinn send without UDP
-GSO: a qdisc takes a GSO burst as one packet.
+acknowledgments) does not queue behind it. Pure and the QUICs send without
+UDP GSO: a qdisc takes a GSO burst as one packet.
bulk a backlog of 1 KiB records for --seconds (Pure: a link and a
- listener, transport.c; quinn: one stream); the goodput from the
- receiver's records in each 100 ms, the first 2 s left out.
+ listener, transport.c; quinn and ngtcp2: one stream); the goodput
+ from the receiver's records in each 100 ms, the first 2 s left out.
paced 50 records a second for --seconds (well within what each carries); the time each took from
queued (written) to handed over (read): p50, p99, max.
- share Pure, quinn or a second TCP flow beside a TCP CUBIC flow, each a
+ share Pure, quinn, ngtcp2 or a second TCP flow beside a TCP CUBIC flow, each a
process of its own, started together, for --share-seconds; each
one's share of the goodput from 20 s on (CUBIC's flows converge
slowly at an RTT of 100 ms). The path's delay wavers by 1 ms: with
@@ -24,7 +24,7 @@ GSO: a qdisc takes a GSO burst as one packet.
than the other's by their phases alone (Floyd and Jacobson, 1992),
and which flow that is says nothing of either.
- python3 research/experiments/020-lossy-paths/run.py [--rounds 3] [--out FILE]
+ python3 research/experiments/020-lossy-paths/run.py [--rounds 3] [--who pure,ngtcp2] [--out FILE]
"""
import argparse
import json
@@ -42,15 +42,16 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
HERE = Path(__file__).resolve().parent
QUINN = ROOT / 'research/experiments/019-idle-links/quic/target/release/idle-quic'
+NGTCP2 = ROOT / 'build/third/ngtcp2-drain'
TC = shutil.which('tc') or '/usr/sbin/tc'
IP = shutil.which('ip') or '/usr/sbin/ip'
RATE_MBIT, SIZE = 50, 1000
-PURE_PORT, QUINN_PORT, TCP_PORT, OTHER_TCP_PORT = 40001, 40002, 40003, 40004 # the receivers, beyond the bottleneck
+PURE_PORT, QUINN_PORT, TCP_PORT, OTHER_TCP_PORT, NGTCP2_PORT = 40001, 40002, 40003, 40004, 40005 # the receivers, beyond the bottleneck
sys.path.insert(0, str(ROOT / 'tests'))
def build(work):
- """The transport harness, quinn's, the two modules and a certificate."""
+ """The transport harness, quinn's, ngtcp2's, the two modules and a certificate."""
import check
import importlib.util
spec = importlib.util.spec_from_file_location('idle_links', ROOT / 'research/experiments/019-idle-links/run.py')
@@ -63,6 +64,7 @@ def build(work):
str(ROOT / 'build/libpure.a'), '-lm', '-pthread', '-ldl', '-o', str(ROOT / 'build/link-transport')],
check=True)
subprocess.run(['cargo', 'build', '--release', '--offline', '--quiet'], cwd=QUINN.parents[2], check=True)
+ subprocess.run(['sh', str(ROOT / 'research/experiments/019-idle-links/ngtcp2/build.sh')], check=True)
for name, text in (('sender', check.LINK_SENDER), ('receiver', check.LINK_RECEIVER)):
(work / f'{name}.aup').write_text(text)
check.compile_module(Path(os.environ.get('AUP', ROOT.parent / 'aup/build/aup')), work / f'{name}.aup',
@@ -144,8 +146,20 @@ def finish_quinn(started, seconds):
return json.loads(line)
-def quinn(work, seconds, rate=0):
- return finish_quinn(start_quinn(work, seconds, rate), seconds)
+def start_ngtcp2(work, seconds, rate=0, count=2000000):
+ """As quinn's: the same messages over ngtcp2 (019's ngtcp2/drain.c)."""
+ env = {**os.environ, 'DRAIN_GSO': '0'}
+ server = subprocess.Popen([str(NGTCP2), 'serve', str(work / 'cert.der'), str(work / 'key.der'), str(NGTCP2_PORT)],
+ stdout=subprocess.PIPE, text=True, env=env)
+ port = json.loads(server.stdout.readline())['port']
+ client = subprocess.Popen([str(NGTCP2), 'drain', str(port), str(count), str(SIZE + 8 + 5), str(rate), str(seconds)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)
+ return server, client
+
+
+def quic(who, work, seconds, rate=0):
+ started = (start_quinn if who == 'quinn' else start_ngtcp2)(work, seconds, rate)
+ return finish_quinn(started, seconds)
def start_tcp(port, seconds):
@@ -216,7 +230,7 @@ def inner(case):
del row['work']
if kind in ('bulk', 'paced'):
rate = 50 if kind == 'paced' else 0
- result = pure(work, seconds, rate) if who == 'pure' else quinn(work, seconds, rate)
+ result = pure(work, seconds, rate) if who == 'pure' else quic(who, work, seconds, rate)
timeline = result['timeline_100ms']
row['mbit'] = goodput(timeline, SIZE + 13, 2.0, seconds)
for key in ('waited_ms_p50', 'waited_ms_p99', 'waited_ms_max', 'resent', 'lost', 'late', 'undone', 'packets'):
@@ -229,7 +243,7 @@ def inner(case):
elif who == 'pure':
theirs = finish_pure(start_pure(work, seconds), seconds)['timeline_100ms']
else:
- theirs = finish_quinn(start_quinn(work, seconds), seconds)['timeline_100ms']
+ theirs = finish_quinn((start_quinn if who == 'quinn' else start_ngtcp2)(work, seconds), seconds)['timeline_100ms']
mine = finish_tcp(tcp, seconds)
row['mbit'] = goodput(theirs, SIZE + 13, 20.0, seconds - 1)
row['tcp_mbit'] = goodput(mine, 1, 20.0, seconds - 1)
@@ -241,17 +255,21 @@ def inner(case):
# ---- Outside
-def cases(seconds, share_seconds):
+def cases(seconds, share_seconds, whom=('pure', 'quinn', 'ngtcp2', 'tcp')):
for rtt in (20, 100):
for loss in (0, 1, 5):
for jitter in (0, 3):
- for who in ('pure', 'quinn'):
- yield {'kind': 'bulk', 'who': who, 'rtt_ms': rtt, 'loss': loss, 'jitter_ms': jitter, 'seconds': seconds}
+ for who in ('pure', 'quinn', 'ngtcp2'):
+ if who in whom:
+ yield {'kind': 'bulk', 'who': who, 'rtt_ms': rtt, 'loss': loss, 'jitter_ms': jitter,
+ 'seconds': seconds}
for loss in (0, 1, 5):
- for who in ('pure', 'quinn'):
- yield {'kind': 'paced', 'who': who, 'rtt_ms': rtt, 'loss': loss, 'jitter_ms': 0, 'seconds': seconds}
- for who in ('pure', 'quinn', 'tcp'):
- yield {'kind': 'share', 'who': who, 'rtt_ms': rtt, 'loss': 0, 'jitter_ms': 1, 'seconds': share_seconds}
+ for who in ('pure', 'quinn', 'ngtcp2'):
+ if who in whom:
+ yield {'kind': 'paced', 'who': who, 'rtt_ms': rtt, 'loss': loss, 'jitter_ms': 0, 'seconds': seconds}
+ for who in ('pure', 'quinn', 'ngtcp2', 'tcp'):
+ if who in whom:
+ yield {'kind': 'share', 'who': who, 'rtt_ms': rtt, 'loss': 0, 'jitter_ms': 1, 'seconds': share_seconds}
def main():
@@ -260,6 +278,7 @@ def main():
parser.add_argument('--seconds', type=int, default=20)
parser.add_argument('--share-seconds', type=int, default=60)
parser.add_argument('--only', default=None, help='kinds to run, for example bulk,share')
+ parser.add_argument('--who', default='pure,quinn,ngtcp2,tcp', help='whom to run, for example pure,ngtcp2')
parser.add_argument('--out', type=Path, default=None)
parser.add_argument('--inner', default=None, help=argparse.SUPPRESS)
parser.add_argument('--tcp', default=None, help=argparse.SUPPRESS)
@@ -278,7 +297,7 @@ def main():
build(work)
only = set(args.only.split(',')) if args.only else None
for round_ in range(args.rounds):
- for case in cases(args.seconds, args.share_seconds):
+ for case in cases(args.seconds, args.share_seconds, tuple(args.who.split(','))):
if only and case['kind'] not in only:
continue
case = {**case, 'round': round_, 'work': str(work)}