aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/019-idle-links/run.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/019-idle-links/run.py')
-rw-r--r--research/experiments/019-idle-links/run.py142
1 files changed, 119 insertions, 23 deletions
diff --git a/research/experiments/019-idle-links/run.py b/research/experiments/019-idle-links/run.py
index 1a179e8..63f88a2 100644
--- a/research/experiments/019-idle-links/run.py
+++ b/research/experiments/019-idle-links/run.py
@@ -16,8 +16,22 @@ clients connect, send one order each and wait until it is stored, and stay
connected at rest; the service's memory is read then; then each sends one
more order over its connection.
+With --restart (M2), n devices each make an order every --period-ms and
+send it until it is stored; at --kill-at seconds the receiver is killed
+(-9) and started again at once: the orders stored each 100 ms, the CPU of
+both sides before and after, the X25519 made (none when the node
+remembers its secrets; --no-memo forgets them), and that every order made
+was applied once. With --quic too, the same for quinn clients, which
+connect again when their connection is lost.
+
+With --first-contact DELAYS (M4), for each one-way delay in ms, in a
+network namespace of its own (unshare -rn) whose loopback delays UDP:
+senders the node never heard from, and new quinn clients, each send one
+order and wait until it is stored, one at a time.
+
python3 research/experiments/019-idle-links/run.py [--counts 1000,10000]
- [--store DIRECTORY] [--out FILE.jsonl] [--quic]
+ [--store DIRECTORY] [--out FILE.jsonl] [--quic] [--restart [--no-memo]]
+ [--first-contact 5,25,50]
"""
import argparse
import json
@@ -197,21 +211,21 @@ def quic(counts, work, window, out):
file.write(line + '\n')
-def recovery(timeline, gap_from_ms, gap_ms):
- """From a timeline of orders stored each 100 ms: the steady rate before
- the gap, and how long after the gap began until a second of storing was
- back at 90 % of it with what waited cleared (the rate seen at least once
- and the backlog gone: the cumulative stored caught up with the steady
- line)."""
- start = gap_from_ms // 100
+def recovery(timeline, killed_ms):
+ """From a timeline of orders stored each 100 ms and when the receiver was
+ killed: the steady rate in the 5 s before; the 100 ms steps after the
+ kill with nothing stored; and how long until the orders stored since the
+ kill caught up with the steady rate (99 %), what waited included."""
+ start = killed_ms // 100
before = timeline[max(0, start - 50):start]
steady = sum(before) / len(before) if before else 0
- total_before = sum(timeline[:start])
- for b in range(start + gap_ms // 100, len(timeline)):
- expected = total_before + steady * (b + 1 - start)
- if sum(timeline[:b + 1]) >= expected * 0.99:
- return {'steady_per_100ms': round(steady, 1), 'caught_up_ms': (b + 1 - start) * 100}
- return {'steady_per_100ms': round(steady, 1), 'caught_up_ms': None}
+ silent = sum(1 for b in range(start, min(len(timeline), start + 100)) if not timeline[b])
+ since = 0
+ for b in range(start, len(timeline)):
+ since += timeline[b]
+ if b > start and since >= steady * (b + 1 - start) * 0.99:
+ return {'steady_per_100ms': round(steady, 1), 'silent_100ms_steps': silent, 'caught_up_ms': (b + 1 - start) * 100}
+ return {'steady_per_100ms': round(steady, 1), 'silent_100ms_steps': silent, 'caught_up_ms': None}
def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
@@ -231,27 +245,33 @@ def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
link_port, info['public_key'], 0, n, period_ms, seconds]],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in generator.stderr: # progress, a line a second
- if json.loads(line).get('t', 0) >= kill_at:
+ t = json.loads(line).get('t', 0)
+ if t >= kill_at:
break
- cpu_before = cpu_ms(node.pid)
+ seen = time.monotonic()
+ cpu_before, senders_before = cpu_ms(node.pid), cpu_ms(generator.pid)
time.sleep(1.0)
steady_cpu = cpu_ms(node.pid) - cpu_before # ms of CPU in the second before the kill
+ senders_steady = cpu_ms(generator.pid) - senders_before
node.kill()
node.wait()
killed = time.monotonic()
+ killed_ms = int((t + killed - seen) * 1000) # on the generator's clock
if not memo:
(work / 'node.key.links').unlink()
node, _ = start(args)
row = {'devices': n, 'period_ms': period_ms, 'memo': memo, 'restart_ms': round((time.monotonic() - killed) * 1000, 1),
- 'steady_cpu_ms_per_s': round(steady_cpu, 1)}
- cpu_start = cpu_ms(node.pid)
+ 'steady_cpu_ms_per_s': round(steady_cpu, 1), 'senders_steady_cpu_ms_per_s': round(senders_steady, 1)}
+ cpu_start, senders_start = cpu_ms(node.pid), cpu_ms(generator.pid)
time.sleep(10)
row['recovery_cpu_ms_first_10s'] = round(cpu_ms(node.pid) - cpu_start, 1)
+ row['senders_cpu_ms_first_10s'] = round(cpu_ms(generator.pid) - senders_start, 1)
out, error = generator.communicate(timeout=600)
result = json.loads(out)
row['generator'] = {k: v for k, v in result.items() if k != 'timeline_100ms'}
row['timeline_100ms'] = result['timeline_100ms']
- row['recovery'] = recovery(result['timeline_100ms'], result['gap_from_ms'], result['longest_gap_ms'])
+ row['killed_ms'] = killed_ms
+ row['recovery'] = recovery(result['timeline_100ms'], killed_ms)
count = view(port, 'count')
row['applied'] = count
assert count == result['made'] == result['stored'], (count, result['made'], result['stored'])
@@ -273,28 +293,97 @@ def restart_quic(n, work, period_ms, seconds, kill_at):
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
began = time.monotonic()
time.sleep(kill_at)
- cpu_before = cpu_ms(server.pid)
+ cpu_before, clients_before = cpu_ms(server.pid), cpu_ms(client.pid)
time.sleep(1.0)
steady_cpu = cpu_ms(server.pid) - cpu_before
+ clients_steady = cpu_ms(client.pid) - clients_before
server.kill()
server.wait()
killed = time.monotonic()
server, _ = start(args)
row = {'clients': n, 'period_ms': period_ms, 'restart_ms': round((time.monotonic() - killed) * 1000, 1),
- 'killed_at_s': round(killed - began, 1), 'steady_cpu_ms_per_s': round(steady_cpu, 1)}
- cpu_start = cpu_ms(server.pid)
+ 'killed_at_s': round(killed - began, 1), 'steady_cpu_ms_per_s': round(steady_cpu, 1),
+ 'senders_steady_cpu_ms_per_s': round(clients_steady, 1)}
+ cpu_start, clients_start = cpu_ms(server.pid), cpu_ms(client.pid)
time.sleep(10)
row['recovery_cpu_ms_first_10s'] = round(cpu_ms(server.pid) - cpu_start, 1)
+ row['senders_cpu_ms_first_10s'] = round(cpu_ms(client.pid) - clients_start, 1)
out, _ = client.communicate(timeout=900)
result = json.loads(out)
row['client'] = {k: v for k, v in result.items() if k != 'timeline_100ms'}
row['timeline_100ms'] = result['timeline_100ms']
- row['recovery'] = recovery(result['timeline_100ms'], result['gap_from_ms'], result['longest_gap_ms'])
+ row['killed_ms'] = int((killed - began) * 1000)
+ row['recovery'] = recovery(result['timeline_100ms'], row['killed_ms'])
server.kill()
server.wait()
return row
+def first_contact(work, delay_ms, count):
+ """M4, run in a network namespace of its own whose loopback delays UDP
+ by delay_ms each way: count senders the node never heard from each send
+ one order and wait until it is stored, one at a time; then count quinn
+ clients each connect, send one and wait. How many round trips each
+ takes is the time over 2 delay_ms."""
+ source = work / 'receiver.aup'
+ source.write_text(check.LINK_RECEIVER)
+ module = work / 'receiver.pmod'
+ check.compile_module(Path(os.environ.get('AUP', ROOT.parent / 'aup/build/aup')), source, module)
+ keys = work / 'keys'
+ subprocess.run([str(ROOT / 'build/links'), 'keys', str(count), str(keys)], check=True)
+ port, link_port = free_port(socket.SOCK_STREAM), free_port(socket.SOCK_DGRAM)
+ node, info = start([ROOT / 'build/pure-serve', '--module', module, '--store', work / 'store', '--port', port,
+ '--key', work / 'node.key', '--link-port', link_port, '--senders', f'{keys}.senders'])
+ sent = subprocess.run([str(a) for a in [ROOT / 'build/links', 'send', keys, module, 'order', '127.0.0.1', link_port,
+ info['public_key'], 0, count, 1, 1]], capture_output=True, text=True)
+ node.terminate()
+ node.communicate(timeout=60)
+ pure = json.loads(sent.stdout)
+ make_certificate(work)
+ binary = Path(__file__).parent / 'quic/target/release/idle-quic'
+ server, info = start([binary, 'serve', work / 'cert.der', work / 'key.der'])
+ fresh = subprocess.run([str(binary), 'connect', str(info['port']), '0', str(count), '1'], input=b'',
+ capture_output=True, timeout=600)
+ server.kill()
+ server.wait()
+ quic = json.loads(fresh.stdout.decode().splitlines()[0])
+ rtt = 2 * delay_ms
+ return {'delay_ms_each_way': delay_ms, 'senders': count,
+ 'pure_ms_p50': pure['stored_ms_p50'], 'pure_ms_p99': pure['stored_ms_p99'],
+ 'pure_round_trips': round(pure['stored_ms_p50'] / rtt, 2),
+ 'quinn_ms_p50': quic['stored_ms_p50'], 'quinn_ms_p99': quic['stored_ms_p99'],
+ 'quinn_round_trips': round(quic['stored_ms_p50'] / rtt, 2)}
+
+
+def in_namespace(delay_ms, count, out):
+ """first_contact in `unshare -rn`, UDP alone through netem (the node's
+ HTTP view stays quick)."""
+ import shutil
+ tc = shutil.which('tc') or '/usr/sbin/tc'
+ ip = shutil.which('ip') or '/usr/sbin/ip'
+ script = f"""
+import json, subprocess, sys, tempfile
+from pathlib import Path
+sys.path.insert(0, {str(Path(__file__).parent)!r})
+import run
+subprocess.run([{ip!r}, 'link', 'set', 'lo', 'up'], check=True)
+for rule in (['qdisc', 'add', 'dev', 'lo', 'root', 'handle', '1:', 'prio'],
+ ['qdisc', 'add', 'dev', 'lo', 'parent', '1:3', 'handle', '30:', 'netem', 'delay', '{delay_ms}ms'],
+ ['filter', 'add', 'dev', 'lo', 'protocol', 'ip', 'parent', '1:0', 'prio', '3', 'u32', 'match', 'ip',
+ 'protocol', '17', '0xff', 'flowid', '1:3']):
+ subprocess.run([{tc!r}] + rule, check=True)
+with tempfile.TemporaryDirectory(prefix='pure-m4-') as d:
+ print(json.dumps(run.first_contact(Path(d), {delay_ms}, {count})))
+"""
+ done = subprocess.run(['unshare', '-rn', sys.executable, '-c', script], capture_output=True, text=True, timeout=1800)
+ assert done.returncode == 0, done.stderr[-3000:]
+ line = done.stdout.strip().splitlines()[-1]
+ print(line, flush=True)
+ if out:
+ with open(out, 'a') as file:
+ file.write(line + '\n')
+
+
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--counts', default='1000,10000,100000,1000000')
@@ -308,8 +397,15 @@ def main():
parser.add_argument('--period-ms', type=int, default=5000)
parser.add_argument('--seconds', type=int, default=40)
parser.add_argument('--kill-at', type=int, default=20)
+ parser.add_argument('--first-contact', type=str, default=None,
+ help='M4: one-way delays in ms (for example 5,25,50), each in a namespace of its own')
args = parser.parse_args()
counts = [int(c) for c in args.counts.split(',')]
+ if args.first_contact:
+ subprocess.run(['cargo', 'build', '--release', '--offline', '--quiet'], cwd=Path(__file__).parent / 'quic', check=True)
+ for delay in args.first_contact.split(','):
+ in_namespace(int(delay), counts[0], args.out)
+ return
if args.restart:
with tempfile.TemporaryDirectory(prefix='pure-m2-', dir=args.store) as directory:
work = Path(directory)