aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs/rto_check.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/044-specs/rto_check.py')
-rw-r--r--research/experiments/044-specs/rto_check.py145
1 files changed, 145 insertions, 0 deletions
diff --git a/research/experiments/044-specs/rto_check.py b/research/experiments/044-specs/rto_check.py
new file mode 100644
index 0000000..be41ffb
--- /dev/null
+++ b/research/experiments/044-specs/rto_check.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+"""Research 140: RFC 6298's retransmission timer written in aup (aup's examples/rto.aup), with periodic ticks.
+
+Each scenario (a seed and a tick period P) is made closed-loop by rto.py's model and a receiver: segments sent at random
+gaps, each arriving half a random round trip later unless its first sending is lost (one in five), the receiver
+acknowledging cumulatively all it holds without a gap, the acknowledgement arriving after the other half; a segment the
+timer sends again arrives. The events (sends, acknowledgements, ticks every P ms) are then given to the app
+through pure-run (time MS before each), on Pure's three paths: after every event its SRTT, RTTVAR, RTO, deadline, the
+segments outstanding and the retransmissions must be the model's, the paths the same. No known implementation is
+compared: the oracle is the model of the same rules. What the ticks cost is measured: ticks a timeout (a one-shot timer
+would be one event), and how late a timeout is seen (below P). Then counts lines of rto.aup and of rto.py's model. One
+JSON line."""
+import json, os, random, struct, subprocess, sys, tempfile
+
+here = os.path.dirname(os.path.abspath(__file__))
+pure = os.path.normpath(os.path.join(here, '../../..'))
+aup = os.environ.get('AUP', os.path.join(pure, '..', 'aup'))
+sys.path.insert(0, here)
+import rto # noqa: E402
+
+START = 1_000_000 # the first event's time (ms since 1970, as now is)
+
+
+def lines(path, stop=None):
+ code = braces = 0
+ for line in open(path):
+ if stop and line.startswith(stop): break
+ text = line.strip()
+ if not text or text.startswith('#'): continue
+ code += 1
+ braces += text == '}'
+ return code, braces
+
+
+def scenario(seed, period, segments=40):
+ """The events the app is given (time, kind, seq), the model's state after each, and the timeouts' lateness. The
+ network: a sending arrives half a random round trip later unless it is lost (a first sending, one in five), and the
+ receiver acknowledges at once, cumulatively, all it holds without a gap; the acknowledgement arrives after the other
+ half."""
+ chooser = random.Random(seed)
+ sender = rto.Sender(period)
+ pending, t = [], START + period # (time, order, kind, seq): arrivals, then acknowledgements, sends, then ticks
+ for seq in range(1, segments + 1):
+ t += chooser.choice([0, 0, 5, 40, 120, 300, 900])
+ pending.append((t, 2, 'transmit', seq))
+ held, through = set(), 0 # the receiver's segments, and the last one it holds every one up to
+
+ def sent(time, seq, first):
+ if not first or chooser.random() > 0.2:
+ half = chooser.randrange(20, 350)
+ pending.append((time + half, 0, 'arrive', seq * 1000 + half))
+
+ events, states, late = [], [], []
+ tick = START
+ while pending or sender.outstanding:
+ pending.sort()
+ if pending and pending[0][0] < tick + period: # the host ticks every P, the timer running or not
+ time, _, kind, seq = pending.pop(0)
+ else:
+ time, kind, seq = tick + period, 'tick', 0
+ tick = time
+ if kind == 'arrive': # the receiver's: not the app's
+ seq, half = divmod(seq, 1000)
+ held.add(seq)
+ while through + 1 in held:
+ through += 1
+ if through:
+ pending.append((time + half, 1, 'ack', through))
+ continue
+ if kind == 'transmit':
+ sender.send(time, seq)
+ sent(time, seq, True)
+ elif kind == 'ack':
+ sender.ack(time, seq)
+ else:
+ before, deadline = len(sender.retransmits), sender.deadline
+ sender.tick(time)
+ if len(sender.retransmits) > before:
+ late.append(time - deadline)
+ sent(time, sender.retransmits[-1][0], False)
+ events.append((time, kind, seq))
+ states.append((sender.srtt, sender.rttvar, sender.rto, sender.deadline, len(sender.outstanding), len(sender.retransmits)))
+ if len(events) > 200000:
+ raise RuntimeError('a scenario without end')
+ return events, states, late
+
+
+def run(module, events, env, work):
+ commands = os.path.join(work, 'rto.commands')
+ with open(commands, 'w') as f:
+ for time, kind, seq in events:
+ f.write(f'time {time}\n')
+ f.write('tick tick\n' if kind == 'tick' else f"message {kind} {struct.pack('<I', seq).hex()}\n")
+ got = []
+ with open(commands) as stdin:
+ process = subprocess.Popen([os.path.join(pure, 'build', 'pure-run'), module], stdin=stdin, stdout=subprocess.PIPE,
+ text=True, env=dict(os.environ, **env))
+ for k, line in enumerate(process.stdout):
+ if k % 2 == 0: continue # a time command's
+ state = json.loads(line)
+ s = state['states']
+ got.append((state['fault'], s['srtt'], s['rttvar'], s['rto'], s['deadline'], len(s['outstanding']),
+ len(s['retransmits'])))
+ process.wait()
+ return got
+
+
+if __name__ == '__main__':
+ report = {'scenarios': 0, 'events': 0, 'timeouts': 0, 'right': {}, 'paths_same': True}
+ costs = {}
+ seeds = int(sys.argv[1]) if len(sys.argv) > 1 else 10
+ with tempfile.TemporaryDirectory() as work:
+ source = open(os.path.join(aup, 'examples', 'rto.aup')).read()
+ for period in (10, 100):
+ module = os.path.join(work, f'rto-{period}.pmod') # its tick and its G the scenario's period
+ with open(os.path.join(work, f'rto-{period}.aup'), 'w') as f:
+ f.write(source.replace('every 10 ms', f'every {period} ms').replace('fn PERIOD = 10 ', f'fn PERIOD = {period} '))
+ subprocess.run([os.path.join(aup, 'build', 'aup'), 'build', os.path.join(work, f'rto-{period}.aup'), '-o', module], check=True)
+ ticks = timeouts = 0
+ lateness = []
+ for seed in range(seeds):
+ events, states, late = scenario(seed, period)
+ outputs = {}
+ for name, env in (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}),
+ ('all', {'PURE_JIT': 'all'})):
+ got = run(module, events, env, work)
+ want = [('none',) + s for s in states]
+ report['right'][name] = report['right'].get(name, 0) + sum(g == w for g, w in zip(got, want))
+ outputs[name] = got
+ report['paths_same'] = report['paths_same'] and outputs['rounds'] == outputs['batches'] == outputs['all']
+ report['scenarios'] += 1
+ report['events'] += len(events)
+ ticks += sum(kind == 'tick' for _, kind, _ in events)
+ timeouts += len(late)
+ lateness += late
+ report['timeouts'] += timeouts
+ costs[period] = {'ticks': ticks, 'timeouts': timeouts, 'ticks_a_timeout': round(ticks / max(timeouts, 1), 1),
+ 'late_mean_ms': round(sum(lateness) / max(len(lateness), 1), 2), 'late_most_ms': max(lateness, default=0)}
+ report['costs'] = costs
+ aup_code, aup_braces = lines(os.path.join(aup, 'examples', 'rto.aup'))
+ python_code, _ = lines(os.path.join(here, 'rto.py'))
+ report.update({'aup_lines': aup_code, 'aup_brace_lines': aup_braces, 'python_lines': python_code})
+ ok = all(n == report['events'] for n in report['right'].values()) and report['paths_same']
+ print(json.dumps(report))
+ sys.exit(0 if ok else 1)