aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 19:57:27 +0100
committereunhhu <[email protected]>2026-09-28 19:57:27 +0100
commit5f8c14f774b81bd6d65d531035eae61b79b1d12c (patch)
tree230b95b30326b4077732d44858bea4eac4204820 /research/experiments
parent354a73d82a3f16d7fdc215c2c63d8fe9afd5ef00 (diff)
downloadpure-5f8c14f774b81bd6d65d531035eae61b79b1d12c.tar.gz
pure-5f8c14f774b81bd6d65d531035eae61b79b1d12c.zip
Measure a restart under load and a first contact (095)
M2 and M4 of design 011. Ten thousand devices send an order every five seconds (2,000 a second); the receiver is killed (-9) at about 21 s and started again at once. With its secrets remembered the node shows no 100 ms step without an order stored, catches up in 0.2 s, spends no more CPU than before and makes no X25519; 34 orders were sent again, and every order made was applied once (three rounds; on the SD card the same, with 0.2 s silent). Without the memo it makes 10,000 X25519, 1.6 s of CPU. quinn under the same load stops for 1.0 s and catches up after 10.9 s: 10,115 new connections, about 7 s more of the service's CPU and 8 s of the clients'. A first contact takes Pure one round trip and QUIC two, at 10, 50 and 100 ms of RTT. run.py --restart and --first-contact make them; the quinn harness gains a fixed port with a reset key from its key, clients that connect again, and a drain for M5.
Diffstat (limited to 'research/experiments')
-rw-r--r--research/experiments/019-idle-links/2026-09-28-first-contact-linux-aarch64.jsonl3
-rw-r--r--research/experiments/019-idle-links/2026-09-28-restart-links-linux-aarch64.jsonl6
-rw-r--r--research/experiments/019-idle-links/2026-09-28-restart-links-sd-linux-aarch64.jsonl1
-rw-r--r--research/experiments/019-idle-links/2026-09-28-restart-quinn-linux-aarch64.jsonl3
-rw-r--r--research/experiments/019-idle-links/quic/src/main.rs71
-rw-r--r--research/experiments/019-idle-links/run.py142
6 files changed, 201 insertions, 25 deletions
diff --git a/research/experiments/019-idle-links/2026-09-28-first-contact-linux-aarch64.jsonl b/research/experiments/019-idle-links/2026-09-28-first-contact-linux-aarch64.jsonl
new file mode 100644
index 0000000..a404b1a
--- /dev/null
+++ b/research/experiments/019-idle-links/2026-09-28-first-contact-linux-aarch64.jsonl
@@ -0,0 +1,3 @@
+{"delay_ms_each_way": 5, "senders": 100, "pure_ms_p50": 10.585, "pure_ms_p99": 12.202, "pure_round_trips": 1.06, "quinn_ms_p50": 20.991, "quinn_ms_p99": 21.499, "quinn_round_trips": 2.1}
+{"delay_ms_each_way": 25, "senders": 100, "pure_ms_p50": 50.932, "pure_ms_p99": 52.325, "pure_round_trips": 1.02, "quinn_ms_p50": 101.153, "quinn_ms_p99": 101.799, "quinn_round_trips": 2.02}
+{"delay_ms_each_way": 50, "senders": 100, "pure_ms_p50": 100.735, "pure_ms_p99": 102.419, "pure_round_trips": 1.01, "quinn_ms_p50": 201.192, "quinn_ms_p99": 202.022, "quinn_round_trips": 2.01}
diff --git a/research/experiments/019-idle-links/2026-09-28-restart-links-linux-aarch64.jsonl b/research/experiments/019-idle-links/2026-09-28-restart-links-linux-aarch64.jsonl
new file mode 100644
index 0000000..de7c076
--- /dev/null
+++ b/research/experiments/019-idle-links/2026-09-28-restart-links-linux-aarch64.jsonl
@@ -0,0 +1,6 @@
+{"devices": 10000, "period_ms": 5000, "memo": true, "restart_ms": 13.7, "steady_cpu_ms_per_s": 130.0, "senders_steady_cpu_ms_per_s": 90.0, "recovery_cpu_ms_first_10s": 1040.0, "senders_cpu_ms_first_10s": 980.0, "generator": {"devices": 10000, "made": 80003, "stored": 80003, "sends": 80037, "again": 34, "stored_ms_p50": 0.535, "stored_ms_p99": 4.147, "stored_ms_max": 203.874, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [183, 207, 191, 208, 191, 212, 193, 193, 211, 193, 194, 212, 195, 197, 195, 214, 196, 196, 196, 214, 197, 198, 198, 199, 197, 198, 198, 200, 200, 200, 199, 216, 200, 201, 200, 200, 183, 199, 218, 182, 201, 202, 200, 200, 200, 202, 203, 202, 201, 201, 213, 200, 198, 200, 199, 200, 200, 200, 199, 199, 200, 200, 200, 200, 199, 201, 201, 201, 201, 201, 200, 199, 201, 191, 200, 200, 199, 202, 201, 201, 201, 202, 202, 194, 208, 191, 201, 203, 202, 201, 200, 201, 191, 204, 202, 204, 191, 203, 201, 202, 199, 189, 213, 202, 197, 199, 203, 193, 200, 200, 200, 202, 202, 201, 200, 202, 191, 201, 201, 199, 200, 201, 202, 203, 201, 196, 205, 195, 205, 194, 206, 196, 205, 196, 193, 206, 195, 205, 205, 194, 205, 194, 205, 195, 206, 194, 200, 207, 196, 196, 203, 203, 191, 208, 200, 201, 197, 205, 189, 202, 202, 203, 204, 193, 203, 203, 190, 199, 206, 205, 191, 201, 203, 200, 201, 202, 202, 190, 204, 201, 201, 203, 199, 194, 202, 202, 202, 193, 203, 204, 201, 201, 191, 202, 205, 194, 202, 205, 192, 205, 200, 200, 200, 203, 197, 202, 203, 190, 199, 201, 165, 203, 238, 203, 193, 205, 203, 193, 196, 201, 208, 199, 197, 199, 198, 198, 200, 200, 211, 189, 200, 200, 201, 205, 200, 200, 196, 207, 194, 207, 194, 199, 205, 196, 196, 206, 195, 209, 196, 198, 204, 203, 192, 203, 202, 201, 192, 204, 203, 193, 202, 200, 200, 198, 210, 199, 200, 200, 199, 201, 198, 199, 201, 200, 199, 199, 201, 201, 198, 202, 199, 201, 200, 198, 200, 203, 194, 205, 203, 197, 193, 206, 197, 204, 194, 206, 197, 195, 205, 197, 202, 201, 200, 201, 203, 203, 190, 206, 201, 192, 209, 200, 199, 192, 201, 200, 199, 207, 201, 201, 196, 201, 203, 196, 203, 200, 198, 200, 200, 202, 197, 200, 202, 202, 201, 191, 200, 199, 203, 203, 201, 197, 205, 190, 208, 202, 201, 191, 202, 203, 198, 198, 198, 210, 198, 198, 197, 199, 199, 205, 202, 202, 192, 201, 202, 202, 202, 192, 204, 196, 203, 203, 194, 203, 202, 199, 196, 204, 203, 193, 203, 202, 203, 193, 204, 201, 197, 204, 193, 206, 195, 198, 204, 202, 199, 204, 194, 203, 201, 196, 11], "killed_ms": 21101, "recovery": {"steady_per_100ms": 199.2, "silent_100ms_steps": 0, "caught_up_ms": 200}, "applied": 80003, "listener": {"listener_packets": 75720, "listener_received": 37860, "listener_deliveries": 37860, "listener_woken": 10000, "listener_remembered": 10000, "listener_swept": 0, "listener_active": 10000}}
+{"devices": 10000, "period_ms": 5000, "memo": true, "restart_ms": 18.9, "steady_cpu_ms_per_s": 130.0, "senders_steady_cpu_ms_per_s": 90.0, "recovery_cpu_ms_first_10s": 1250.0, "senders_cpu_ms_first_10s": 1090.0, "generator": {"devices": 10000, "made": 80003, "stored": 80003, "sends": 80037, "again": 34, "stored_ms_p50": 0.572, "stored_ms_p99": 4.115, "stored_ms_max": 205.52, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [183, 208, 190, 212, 192, 191, 208, 208, 192, 191, 210, 208, 192, 192, 212, 193, 210, 193, 194, 214, 196, 196, 197, 200, 199, 210, 198, 198, 200, 196, 197, 196, 214, 195, 200, 199, 198, 196, 198, 214, 198, 197, 198, 198, 198, 198, 215, 199, 198, 198, 205, 199, 199, 209, 199, 201, 195, 200, 199, 199, 201, 200, 200, 201, 197, 203, 203, 194, 199, 204, 199, 196, 202, 200, 203, 200, 202, 200, 199, 200, 204, 200, 200, 200, 193, 201, 201, 202, 202, 191, 201, 200, 200, 202, 202, 202, 202, 200, 201, 200, 200, 196, 198, 199, 199, 198, 209, 198, 198, 200, 200, 199, 201, 201, 201, 198, 196, 207, 198, 200, 199, 204, 192, 204, 195, 202, 204, 203, 198, 198, 203, 202, 191, 202, 203, 200, 200, 202, 200, 202, 200, 200, 199, 191, 203, 199, 202, 202, 203, 193, 199, 208, 200, 199, 199, 199, 198, 198, 199, 199, 211, 198, 199, 197, 205, 197, 198, 199, 198, 200, 202, 200, 200, 200, 203, 200, 201, 199, 200, 201, 196, 205, 195, 205, 195, 204, 198, 196, 206, 196, 204, 195, 205, 194, 207, 194, 205, 196, 206, 195, 203, 201, 201, 191, 206, 201, 202, 192, 201, 203, 157, 210, 234, 199, 200, 199, 199, 200, 200, 202, 199, 199, 203, 200, 191, 201, 202, 201, 201, 202, 202, 195, 199, 204, 193, 203, 204, 204, 191, 209, 191, 204, 204, 190, 206, 205, 193, 205, 193, 206, 198, 197, 198, 209, 199, 198, 197, 197, 210, 199, 198, 203, 194, 205, 194, 204, 193, 205, 205, 195, 197, 205, 195, 205, 195, 206, 194, 195, 211, 193, 207, 193, 204, 202, 195, 203, 203, 194, 196, 204, 206, 195, 204, 202, 194, 204, 203, 193, 205, 193, 200, 200, 200, 200, 202, 202, 201, 201, 201, 201, 201, 201, 190, 201, 205, 198, 202, 201, 200, 202, 191, 202, 200, 205, 193, 202, 203, 203, 192, 207, 196, 205, 196, 198, 200, 207, 195, 197, 207, 196, 196, 208, 197, 197, 198, 209, 197, 195, 209, 198, 194, 201, 204, 196, 202, 204, 194, 204, 202, 193, 204, 203, 195, 205, 195, 205, 194, 205, 193, 207, 196, 204, 194, 205, 195, 207, 194, 206, 195, 195, 209, 194, 207, 195, 197, 208, 195, 195, 206, 196, 209, 196, 197, 196, 207, 196, 206, 194, 206, 195, 11], "killed_ms": 21102, "recovery": {"steady_per_100ms": 199.0, "silent_100ms_steps": 0, "caught_up_ms": 200}, "applied": 80003, "listener": {"listener_packets": 75724, "listener_received": 37862, "listener_deliveries": 37862, "listener_woken": 10000, "listener_remembered": 10000, "listener_swept": 0, "listener_active": 10000}}
+{"devices": 10000, "period_ms": 5000, "memo": true, "restart_ms": 13.4, "steady_cpu_ms_per_s": 110.0, "senders_steady_cpu_ms_per_s": 80.0, "recovery_cpu_ms_first_10s": 1080.0, "senders_cpu_ms_first_10s": 990.0, "generator": {"devices": 10000, "made": 80003, "stored": 80003, "sends": 80025, "again": 22, "stored_ms_p50": 0.542, "stored_ms_p99": 4.259, "stored_ms_max": 206.349, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [184, 207, 190, 208, 190, 207, 191, 210, 205, 186, 199, 210, 193, 211, 191, 211, 192, 192, 210, 192, 200, 212, 195, 195, 192, 207, 201, 197, 198, 215, 200, 197, 199, 197, 198, 199, 199, 199, 199, 198, 201, 199, 201, 203, 202, 201, 204, 201, 201, 202, 201, 198, 200, 199, 210, 192, 199, 201, 201, 202, 195, 210, 192, 198, 201, 202, 200, 200, 201, 201, 202, 201, 202, 191, 201, 203, 202, 194, 204, 195, 206, 194, 206, 196, 206, 196, 205, 195, 206, 194, 196, 207, 194, 208, 195, 197, 206, 200, 195, 203, 199, 200, 200, 200, 199, 198, 199, 201, 201, 201, 202, 199, 200, 201, 200, 199, 199, 203, 201, 202, 191, 204, 201, 202, 201, 201, 200, 198, 201, 201, 191, 206, 201, 202, 195, 201, 201, 200, 202, 201, 191, 201, 203, 201, 202, 203, 194, 199, 204, 195, 202, 200, 199, 202, 203, 202, 191, 201, 202, 202, 204, 191, 204, 203, 202, 191, 203, 202, 204, 193, 205, 191, 206, 194, 205, 203, 200, 194, 204, 195, 198, 201, 209, 202, 187, 210, 193, 201, 201, 200, 202, 203, 203, 199, 192, 202, 203, 202, 202, 191, 201, 208, 199, 198, 199, 198, 198, 200, 199, 199, 181, 204, 217, 205, 195, 200, 203, 203, 195, 205, 194, 197, 207, 195, 206, 195, 206, 194, 207, 194, 207, 198, 201, 190, 201, 203, 203, 202, 190, 203, 203, 202, 192, 204, 198, 206, 200, 197, 200, 198, 205, 204, 194, 204, 193, 205, 193, 205, 206, 198, 194, 201, 201, 201, 201, 200, 201, 201, 202, 189, 200, 205, 201, 202, 201, 201, 192, 200, 202, 202, 205, 191, 202, 201, 202, 202, 193, 202, 203, 202, 193, 201, 202, 202, 193, 207, 192, 206, 199, 199, 201, 198, 199, 209, 190, 206, 203, 199, 201, 200, 201, 200, 200, 200, 192, 200, 201, 200, 202, 202, 202, 200, 199, 201, 200, 199, 201, 200, 201, 201, 191, 202, 201, 199, 197, 202, 204, 201, 203, 191, 202, 202, 201, 201, 193, 208, 196, 204, 195, 196, 205, 201, 204, 191, 203, 201, 204, 202, 192, 203, 205, 192, 202, 202, 204, 191, 202, 200, 205, 193, 207, 190, 206, 196, 201, 201, 209, 199, 200, 198, 202, 190, 200, 202, 204, 201, 201, 200, 201, 193, 202, 202, 204, 191, 200, 202, 202, 202, 192, 203, 12], "killed_ms": 21101, "recovery": {"steady_per_100ms": 199.4, "silent_100ms_steps": 0, "caught_up_ms": 200}, "applied": 80003, "listener": {"listener_packets": 75714, "listener_received": 37857, "listener_deliveries": 37857, "listener_woken": 10000, "listener_remembered": 10000, "listener_swept": 0, "listener_active": 10000}}
+{"devices": 10000, "period_ms": 5000, "memo": false, "restart_ms": 19.8, "steady_cpu_ms_per_s": 140.0, "senders_steady_cpu_ms_per_s": 100.0, "recovery_cpu_ms_first_10s": 2730.0, "senders_cpu_ms_first_10s": 860.0, "generator": {"devices": 10000, "made": 80002, "stored": 80002, "sends": 80036, "again": 34, "stored_ms_p50": 0.574, "stored_ms_p99": 4.641, "stored_ms_max": 221.053, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [186, 194, 196, 214, 195, 195, 212, 196, 196, 197, 192, 203, 215, 196, 197, 196, 196, 213, 196, 198, 197, 199, 197, 200, 216, 197, 200, 198, 200, 199, 199, 203, 199, 202, 186, 203, 203, 201, 201, 200, 201, 203, 201, 200, 202, 184, 207, 205, 202, 201, 201, 200, 206, 197, 201, 201, 201, 199, 199, 200, 201, 201, 200, 201, 201, 201, 200, 199, 201, 199, 201, 191, 209, 192, 199, 202, 203, 194, 204, 204, 199, 193, 202, 203, 203, 191, 202, 201, 200, 199, 201, 202, 200, 204, 200, 192, 203, 206, 195, 206, 190, 202, 201, 203, 203, 190, 202, 202, 203, 202, 190, 206, 201, 203, 202, 191, 203, 205, 197, 194, 202, 205, 203, 193, 203, 204, 193, 204, 192, 208, 192, 207, 204, 196, 197, 206, 194, 205, 194, 205, 193, 209, 195, 196, 206, 196, 206, 195, 206, 194, 201, 202, 201, 203, 192, 201, 202, 201, 202, 194, 202, 203, 201, 192, 203, 203, 204, 191, 203, 204, 192, 205, 204, 195, 204, 196, 204, 194, 204, 194, 205, 195, 206, 195, 206, 194, 207, 195, 203, 196, 205, 196, 205, 197, 195, 206, 195, 207, 195, 207, 192, 202, 202, 201, 193, 203, 202, 202, 203, 191, 157, 205, 175, 260, 194, 211, 193, 194, 211, 197, 193, 196, 212, 195, 197, 212, 196, 194, 195, 215, 195, 197, 200, 196, 211, 186, 214, 181, 210, 200, 197, 198, 215, 182, 202, 202, 200, 198, 216, 199, 193, 192, 210, 192, 211, 192, 211, 192, 193, 211, 209, 202, 199, 190, 205, 203, 193, 204, 201, 197, 201, 204, 200, 200, 194, 201, 202, 201, 200, 197, 202, 203, 201, 201, 194, 201, 203, 192, 203, 203, 201, 203, 201, 192, 205, 201, 202, 200, 190, 202, 198, 200, 210, 198, 202, 199, 201, 198, 200, 195, 202, 196, 203, 200, 203, 200, 200, 198, 200, 202, 201, 197, 195, 204, 197, 208, 196, 204, 194, 196, 205, 205, 194, 206, 196, 197, 202, 205, 194, 206, 195, 206, 194, 205, 196, 197, 206, 194, 206, 195, 201, 202, 202, 201, 194, 200, 203, 202, 201, 191, 202, 203, 202, 202, 193, 203, 204, 192, 205, 204, 195, 202, 193, 204, 196, 204, 205, 193, 204, 195, 203, 198, 202, 206, 194, 206, 195, 196, 207, 194, 206, 195, 205, 195, 196, 206, 196, 203, 194, 204, 11], "killed_ms": 21102, "recovery": {"steady_per_100ms": 199.1, "silent_100ms_steps": 0, "caught_up_ms": 300}, "applied": 80002, "listener": {"listener_packets": 75710, "listener_received": 37855, "listener_deliveries": 37855, "listener_woken": 10000, "listener_remembered": 0, "listener_swept": 0, "listener_active": 10000}}
+{"devices": 10000, "period_ms": 5000, "memo": false, "restart_ms": 14.6, "steady_cpu_ms_per_s": 150.0, "senders_steady_cpu_ms_per_s": 100.0, "recovery_cpu_ms_first_10s": 2590.0, "senders_cpu_ms_first_10s": 830.0, "generator": {"devices": 10000, "made": 80011, "stored": 80011, "sends": 80045, "again": 34, "stored_ms_p50": 0.545, "stored_ms_p99": 4.54, "stored_ms_max": 222.224, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [184, 209, 191, 209, 197, 191, 211, 191, 210, 194, 195, 201, 193, 212, 196, 194, 213, 195, 196, 198, 196, 215, 189, 194, 207, 199, 199, 198, 199, 202, 200, 200, 199, 198, 199, 202, 201, 200, 200, 202, 200, 198, 205, 199, 199, 200, 199, 201, 200, 202, 214, 188, 206, 209, 199, 200, 195, 200, 202, 201, 199, 200, 201, 201, 200, 201, 198, 199, 200, 201, 200, 202, 192, 201, 203, 204, 193, 203, 204, 193, 207, 196, 205, 194, 204, 194, 207, 193, 205, 196, 206, 194, 206, 194, 207, 193, 206, 193, 206, 197, 204, 193, 201, 208, 189, 208, 200, 198, 201, 201, 199, 201, 192, 211, 198, 199, 200, 200, 199, 203, 200, 190, 201, 201, 201, 200, 205, 201, 201, 192, 201, 204, 193, 204, 202, 200, 200, 200, 201, 201, 201, 191, 200, 201, 201, 201, 202, 201, 202, 202, 199, 201, 192, 203, 202, 200, 194, 201, 204, 204, 191, 202, 204, 192, 203, 204, 204, 193, 205, 197, 204, 193, 207, 192, 205, 192, 208, 203, 192, 206, 192, 205, 204, 194, 206, 192, 207, 193, 204, 206, 194, 205, 196, 196, 206, 195, 208, 195, 196, 209, 187, 197, 207, 203, 205, 201, 190, 210, 190, 200, 156, 210, 231, 195, 210, 186, 202, 204, 193, 211, 204, 196, 196, 195, 210, 193, 211, 197, 195, 196, 202, 195, 212, 194, 195, 212, 195, 194, 214, 195, 197, 197, 195, 215, 196, 197, 205, 196, 197, 199, 209, 173, 212, 216, 189, 207, 191, 207, 192, 210, 208, 199, 201, 199, 197, 199, 199, 199, 201, 201, 203, 202, 199, 200, 198, 199, 201, 199, 200, 202, 202, 201, 200, 199, 199, 199, 200, 200, 200, 201, 195, 202, 203, 194, 206, 201, 203, 190, 201, 201, 202, 198, 199, 199, 203, 201, 203, 200, 193, 209, 195, 205, 191, 204, 201, 203, 193, 202, 203, 194, 204, 204, 202, 190, 202, 203, 203, 202, 199, 195, 196, 206, 204, 190, 201, 200, 202, 201, 200, 203, 203, 191, 201, 202, 203, 201, 193, 200, 201, 205, 200, 200, 198, 198, 198, 199, 200, 210, 199, 199, 201, 199, 199, 199, 199, 198, 199, 199, 209, 193, 202, 200, 200, 200, 200, 199, 201, 200, 198, 202, 202, 193, 204, 205, 193, 204, 194, 204, 204, 195, 200, 205, 194, 204, 195, 204, 193, 203, 206, 203, 12], "killed_ms": 21002, "recovery": {"steady_per_100ms": 199.8, "silent_100ms_steps": 0, "caught_up_ms": 300}, "applied": 80011, "listener": {"listener_packets": 75854, "listener_received": 37927, "listener_deliveries": 37927, "listener_woken": 10000, "listener_remembered": 0, "listener_swept": 0, "listener_active": 10000}}
+{"devices": 10000, "period_ms": 5000, "memo": false, "restart_ms": 13.6, "steady_cpu_ms_per_s": 110.0, "senders_steady_cpu_ms_per_s": 80.0, "recovery_cpu_ms_first_10s": 2740.0, "senders_cpu_ms_first_10s": 860.0, "generator": {"devices": 10000, "made": 80005, "stored": 80005, "sends": 80027, "again": 22, "stored_ms_p50": 0.57, "stored_ms_p99": 4.765, "stored_ms_max": 212.433, "longest_gap_ms": 0, "gap_from_ms": 0}, "timeline_100ms": [176, 203, 200, 203, 202, 201, 204, 190, 201, 197, 198, 215, 191, 205, 197, 200, 197, 207, 201, 196, 196, 197, 204, 204, 203, 197, 199, 184, 213, 202, 199, 200, 200, 202, 203, 194, 201, 201, 200, 200, 202, 200, 204, 185, 201, 205, 204, 202, 200, 200, 212, 200, 199, 197, 198, 206, 200, 197, 198, 197, 208, 196, 200, 201, 198, 201, 200, 199, 199, 202, 200, 199, 203, 193, 201, 208, 193, 205, 192, 205, 205, 192, 206, 196, 204, 199, 194, 205, 198, 201, 204, 193, 205, 196, 207, 200, 196, 194, 206, 196, 199, 200, 201, 203, 202, 204, 192, 203, 202, 202, 191, 201, 202, 204, 202, 200, 192, 201, 203, 201, 204, 191, 203, 206, 193, 203, 193, 205, 203, 194, 203, 205, 195, 205, 196, 195, 206, 193, 205, 205, 194, 205, 195, 203, 200, 194, 205, 194, 206, 195, 202, 201, 202, 203, 202, 189, 204, 201, 202, 203, 196, 196, 203, 205, 191, 201, 203, 202, 201, 196, 202, 203, 199, 195, 199, 199, 202, 200, 200, 203, 202, 200, 200, 202, 192, 200, 202, 200, 203, 194, 200, 203, 199, 201, 205, 201, 201, 191, 203, 201, 197, 198, 209, 198, 201, 199, 199, 199, 199, 199, 170, 209, 209, 205, 200, 194, 210, 193, 211, 193, 193, 210, 198, 189, 214, 195, 196, 196, 212, 195, 195, 202, 194, 214, 197, 196, 197, 203, 207, 197, 196, 197, 198, 215, 199, 196, 198, 198, 203, 203, 197, 192, 210, 192, 209, 192, 210, 192, 194, 209, 205, 200, 200, 199, 201, 201, 197, 202, 201, 204, 201, 199, 200, 201, 193, 204, 205, 193, 195, 202, 209, 195, 205, 194, 203, 200, 196, 207, 194, 206, 194, 206, 195, 206, 195, 194, 208, 194, 198, 206, 202, 203, 189, 201, 202, 201, 203, 204, 194, 205, 192, 204, 204, 192, 206, 203, 193, 205, 195, 205, 192, 204, 205, 193, 206, 193, 205, 195, 207, 195, 205, 194, 206, 195, 206, 194, 206, 197, 196, 198, 208, 194, 206, 192, 201, 200, 199, 207, 193, 205, 201, 197, 207, 196, 196, 200, 198, 210, 200, 200, 199, 198, 199, 198, 198, 200, 200, 200, 201, 201, 199, 200, 201, 197, 202, 204, 202, 200, 192, 202, 203, 201, 202, 196, 201, 204, 191, 204, 204, 200, 190, 202, 201, 201, 196, 209, 196, 195, 209, 197, 11], "killed_ms": 21101, "recovery": {"steady_per_100ms": 199.4, "silent_100ms_steps": 0, "caught_up_ms": 200}, "applied": 80005, "listener": {"listener_packets": 75742, "listener_received": 37871, "listener_deliveries": 37871, "listener_woken": 10000, "listener_remembered": 0, "listener_swept": 0, "listener_active": 10000}}
diff --git a/research/experiments/019-idle-links/2026-09-28-restart-links-sd-linux-aarch64.jsonl b/research/experiments/019-idle-links/2026-09-28-restart-links-sd-linux-aarch64.jsonl
new file mode 100644
index 0000000..7cbf031
--- /dev/null
+++ b/research/experiments/019-idle-links/2026-09-28-restart-links-sd-linux-aarch64.jsonl
@@ -0,0 +1 @@
+{"devices": 10000, "period_ms": 5000, "memo": true, "restart_ms": 28.6, "steady_cpu_ms_per_s": 100.0, "senders_steady_cpu_ms_per_s": 70.0, "recovery_cpu_ms_first_10s": 1060.0, "senders_cpu_ms_first_10s": 940.0, "generator": {"devices": 10000, "made": 80007, "stored": 80007, "sends": 80276, "again": 269, "stored_ms_p50": 4.092, "stored_ms_p99": 110.889, "stored_ms_max": 267.41, "longest_gap_ms": 200, "gap_from_ms": 17700}, "timeline_100ms": [186, 197, 198, 206, 192, 190, 221, 181, 167, 239, 211, 202, 161, 136, 296, 203, 200, 187, 203, 201, 200, 194, 214, 205, 193, 128, 240, 238, 196, 197, 199, 197, 205, 195, 206, 198, 209, 193, 195, 207, 197, 196, 203, 198, 207, 163, 120, 306, 203, 157, 249, 198, 200, 212, 194, 187, 187, 213, 212, 162, 221, 212, 196, 214, 192, 202, 200, 194, 190, 222, 149, 249, 197, 199, 190, 214, 202, 195, 200, 36, 356, 187, 223, 161, 231, 196, 202, 202, 208, 194, 207, 197, 195, 199, 189, 216, 155, 252, 191, 197, 192, 207, 203, 196, 212, 196, 188, 208, 144, 246, 216, 195, 190, 207, 211, 191, 200, 208, 195, 206, 176, 213, 204, 202, 195, 190, 186, 201, 228, 192, 202, 203, 201, 206, 180, 215, 195, 193, 208, 204, 201, 192, 191, 176, 234, 214, 197, 196, 191, 173, 234, 180, 224, 192, 200, 213, 189, 205, 186, 212, 193, 213, 194, 199, 203, 191, 199, 185, 171, 240, 196, 215, 134, 264, 199, 176, 210, 0, 0, 619, 194, 200, 200, 113, 269, 217, 95, 298, 201, 216, 191, 203, 164, 236, 204, 200, 200, 194, 201, 199, 202, 201, 202, 197, 200, 200, 206, 163, 136, 301, 135, 180, 188, 291, 158, 232, 211, 205, 200, 190, 205, 193, 210, 202, 201, 199, 166, 229, 200, 201, 197, 207, 195, 196, 170, 235, 138, 255, 202, 186, 222, 201, 176, 16, 398, 206, 208, 163, 224, 198, 201, 205, 131, 261, 198, 198, 218, 192, 151, 235, 201, 210, 100, 308, 198, 192, 214, 189, 197, 185, 217, 206, 191, 214, 188, 184, 215, 211, 188, 68, 0, 541, 190, 214, 193, 123, 154, 299, 231, 152, 44, 0, 591, 216, 187, 192, 208, 203, 208, 202, 173, 218, 205, 198, 200, 132, 269, 194, 204, 198, 156, 249, 190, 197, 186, 209, 212, 204, 203, 198, 192, 206, 195, 207, 186, 207, 205, 199, 206, 202, 192, 194, 169, 141, 293, 199, 198, 199, 207, 200, 203, 187, 204, 212, 201, 191, 202, 198, 198, 196, 205, 199, 202, 199, 196, 210, 185, 213, 198, 194, 200, 210, 204, 194, 173, 230, 195, 193, 207, 201, 84, 325, 194, 196, 175, 224, 201, 185, 213, 170, 232, 192, 178, 231, 152, 243, 189, 176, 242, 184, 203, 210, 205, 201, 181, 218, 193, 210, 195, 102, 117], "killed_ms": 21102, "recovery": {"steady_per_100ms": 198.9, "silent_100ms_steps": 2, "caught_up_ms": 300}, "applied": 80007, "listener": {"listener_packets": 75909, "listener_received": 38028, "listener_deliveries": 37881, "listener_woken": 10000, "listener_remembered": 10000, "listener_swept": 0, "listener_active": 10000}}
diff --git a/research/experiments/019-idle-links/2026-09-28-restart-quinn-linux-aarch64.jsonl b/research/experiments/019-idle-links/2026-09-28-restart-quinn-linux-aarch64.jsonl
new file mode 100644
index 0000000..839d380
--- /dev/null
+++ b/research/experiments/019-idle-links/2026-09-28-restart-quinn-linux-aarch64.jsonl
@@ -0,0 +1,3 @@
+{"clients": 10000, "period_ms": 5000, "restart_ms": 1.6, "killed_at_s": 26.0, "steady_cpu_ms_per_s": 120.0, "senders_steady_cpu_ms_per_s": 120.0, "recovery_cpu_ms_first_10s": 8290.0, "senders_cpu_ms_first_10s": 9140.0, "client": {"clients": 10000, "made": 89998, "stored": 89998, "again": 10173, "stored_ms_p50": 0.148, "stored_ms_p99": 5826.357, "stored_ms_max": 11517.095, "longest_gap_ms": 900, "gap_from_ms": 26100}, "timeline_100ms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95, 89, 73, 196, 347, 236, 222, 201, 206, 156, 138, 138, 137, 210, 293, 248, 166, 183, 200, 198, 192, 124, 128, 135, 191, 291, 230, 218, 208, 108, 131, 157, 110, 161, 239, 292, 184, 160, 208, 201, 198, 180, 131, 97, 95, 121, 190, 157, 148, 156, 291, 281, 220, 179, 234, 224, 379, 465, 272, 215, 202, 206, 221, 344, 247, 293, 223, 209, 200, 200, 198, 202, 198, 200, 202, 201, 219, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 198, 202, 200, 200, 200, 201, 197, 202, 200, 200, 200, 200, 200, 200, 202, 198, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 203, 199, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 203, 199, 200, 200, 200, 200, 200, 198, 202, 200, 201, 199, 200, 200, 200, 200, 202, 198, 202, 198, 200, 200, 200, 200, 200, 200, 200, 198, 204, 198, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 198, 200, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 10, 78, 93, 166, 136, 137, 126, 128, 128, 116, 65, 54, 66, 100, 126, 116, 105, 122, 109, 108, 69, 48, 95, 95, 92, 101, 114, 102, 109, 101, 101, 78, 71, 54, 132, 101, 112, 102, 98, 284, 223, 111, 94, 220, 326, 387, 348, 335, 380, 336, 322, 195, 114, 188, 241, 296, 306, 409, 342, 346, 299, 273, 163, 189, 237, 321, 324, 266, 370, 351, 334, 287, 151, 180, 233, 298, 288, 316, 339, 354, 331, 307, 202, 86, 154, 195, 211, 279, 309, 483, 465, 462, 435, 243, 191, 173, 268, 324, 485, 631, 310, 264, 203, 221, 379, 301, 277, 276, 229, 216, 208, 202, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 202, 198, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 199, 201, 200, 200, 200, 194, 206, 199, 201, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 199, 199, 202, 198, 202, 200, 200, 200, 200, 200, 200], "killed_ms": 26010, "recovery": {"steady_per_100ms": 200.0, "silent_100ms_steps": 9, "caught_up_ms": 11700}}
+{"clients": 10000, "period_ms": 5000, "restart_ms": 1.3, "killed_at_s": 26.0, "steady_cpu_ms_per_s": 90.0, "senders_steady_cpu_ms_per_s": 90.0, "recovery_cpu_ms_first_10s": 8300.0, "senders_cpu_ms_first_10s": 9120.0, "client": {"clients": 10000, "made": 89998, "stored": 89998, "again": 10069, "stored_ms_p50": 0.136, "stored_ms_p99": 5348.757, "stored_ms_max": 11197.085, "longest_gap_ms": 1000, "gap_from_ms": 26000}, "timeline_100ms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 113, 74, 133, 239, 244, 237, 209, 204, 178, 174, 148, 124, 156, 197, 298, 235, 193, 193, 192, 201, 195, 109, 116, 109, 206, 267, 279, 233, 205, 113, 156, 112, 127, 235, 199, 237, 180, 187, 206, 205, 202, 147, 81, 64, 128, 183, 136, 176, 158, 150, 297, 262, 167, 230, 268, 284, 477, 383, 239, 220, 205, 200, 228, 284, 279, 292, 238, 201, 202, 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 200, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 198, 202, 200, 200, 200, 198, 202, 198, 202, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 203, 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 201, 199, 200, 198, 202, 200, 200, 200, 200, 201, 199, 200, 200, 200, 200, 200, 200, 201, 199, 200, 199, 201, 200, 200, 198, 202, 198, 202, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 70, 45, 85, 117, 135, 134, 135, 128, 128, 122, 81, 74, 92, 98, 99, 111, 117, 109, 116, 118, 90, 44, 71, 76, 105, 101, 102, 102, 112, 121, 110, 81, 49, 85, 81, 114, 103, 95, 82, 257, 270, 214, 136, 278, 279, 275, 372, 316, 379, 339, 337, 292, 165, 255, 184, 251, 348, 364, 348, 348, 337, 316, 211, 173, 262, 226, 329, 364, 309, 362, 356, 335, 301, 152, 211, 241, 208, 365, 392, 352, 286, 320, 340, 271, 221, 194, 199, 208, 248, 456, 485, 454, 431, 376, 421, 434, 306, 271, 266, 220, 216, 200, 200, 219, 235, 206, 204, 202, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 203, 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 202, 198, 200, 200, 200, 200, 198, 200, 202, 198, 202, 200, 200, 200, 198, 202, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 198, 202, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 198, 202, 198, 2], "killed_ms": 26008, "recovery": {"steady_per_100ms": 200.0, "silent_100ms_steps": 10, "caught_up_ms": 10900}}
+{"clients": 10000, "period_ms": 5000, "restart_ms": 1.5, "killed_at_s": 26.0, "steady_cpu_ms_per_s": 120.0, "senders_steady_cpu_ms_per_s": 110.0, "recovery_cpu_ms_first_10s": 8420.0, "senders_cpu_ms_first_10s": 9150.0, "client": {"clients": 10000, "made": 89998, "stored": 89998, "again": 10115, "stored_ms_p50": 0.148, "stored_ms_p99": 5270.907, "stored_ms_max": 10060.656, "longest_gap_ms": 1000, "gap_from_ms": 26000}, "timeline_100ms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 50, 109, 169, 287, 285, 223, 210, 207, 162, 124, 120, 78, 292, 232, 288, 205, 196, 194, 196, 193, 139, 98, 131, 184, 292, 258, 212, 199, 109, 150, 148, 98, 135, 200, 279, 207, 208, 229, 203, 200, 179, 116, 71, 74, 123, 141, 158, 159, 156, 319, 264, 226, 209, 315, 218, 359, 387, 353, 195, 218, 205, 221, 295, 318, 243, 240, 222, 203, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 198, 200, 202, 200, 200, 200, 200, 200, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 200, 202, 200, 200, 200, 200, 200, 198, 202, 200, 198, 202, 200, 199, 201, 200, 200, 198, 202, 200, 200, 200, 198, 202, 200, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 199, 201, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 198, 202, 200, 198, 202, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 29, 45, 110, 144, 154, 142, 127, 140, 130, 110, 70, 57, 98, 131, 113, 109, 116, 118, 125, 123, 75, 64, 94, 97, 117, 102, 110, 101, 112, 117, 104, 49, 23, 88, 93, 129, 120, 108, 118, 269, 245, 192, 142, 216, 312, 347, 302, 361, 386, 344, 313, 250, 170, 200, 227, 333, 354, 409, 320, 331, 332, 309, 185, 238, 232, 278, 337, 350, 343, 323, 338, 341, 236, 242, 232, 215, 244, 414, 363, 313, 284, 303, 290, 236, 287, 209, 288, 309, 308, 462, 449, 452, 462, 329, 281, 305, 306, 260, 226, 222, 200, 200, 200, 200, 200, 201, 199, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 201, 199, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 178, 220, 202, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 202, 200, 200], "killed_ms": 26011, "recovery": {"steady_per_100ms": 200.0, "silent_100ms_steps": 10, "caught_up_ms": 10700}}
diff --git a/research/experiments/019-idle-links/quic/src/main.rs b/research/experiments/019-idle-links/quic/src/main.rs
index cdb0376..28cda77 100644
--- a/research/experiments/019-idle-links/quic/src/main.rs
+++ b/research/experiments/019-idle-links/quic/src/main.rs
@@ -20,6 +20,10 @@
//! connects again and sends it again. Then a line of JSON: orders made
//! and stored, connections made again, the orders stored in each
//! 100 ms, the time from an order made to it stored
+//! idle-quic drain PORT COUNT SIZE
+//! one client sends COUNT messages of SIZE bytes over one stream
+//! (each after its u32 length) and waits until the service says it
+//! read them all; a line of JSON: how long
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
@@ -72,7 +76,17 @@ fn serve(cert: Vec<u8>, key: Vec<u8>, port: u16) {
let Ok(connection) = incoming.await else { return };
kept.lock().unwrap().push(connection.clone());
while let Ok((mut send, mut recv)) = connection.accept_bi().await {
- let Ok(bytes) = recv.read_to_end(1024).await else { return };
+ let mut first = [0u8; 4];
+ if recv.read_exact(&mut first).await.is_err() {
+ return;
+ }
+ if first == *DRAIN {
+ drain(&mut send, &mut recv).await;
+ continue;
+ }
+ let Ok(rest) = recv.read_to_end(1024).await else { return };
+ let mut bytes = first.to_vec();
+ bytes.extend_from_slice(&rest);
if bytes.len() < 16 {
return;
}
@@ -99,6 +113,32 @@ fn serve(cert: Vec<u8>, key: Vec<u8>, port: u16) {
});
}
+/// A drain's stream begins with these bytes; an order's never does (a client number that large).
+const DRAIN: &[u8; 4] = b"pure";
+
+/// Messages each after its u32 length until a length of 0; then how many, as the answer.
+async fn drain(send: &mut quinn::SendStream, recv: &mut quinn::RecvStream) {
+ let mut count = 0u64;
+ let mut buffer = Vec::new();
+ loop {
+ let mut length = [0u8; 4];
+ if recv.read_exact(&mut length).await.is_err() {
+ return;
+ }
+ let length = u32::from_le_bytes(length) as usize;
+ if length == 0 {
+ break;
+ }
+ buffer.resize(length, 0);
+ if recv.read_exact(&mut buffer).await.is_err() {
+ return;
+ }
+ count += 1;
+ }
+ let _ = send.write_all(&count.to_le_bytes()).await;
+ let _ = send.finish();
+}
+
#[derive(Debug)]
struct Anyone; // the test's own service: its certificate is not checked
@@ -308,12 +348,39 @@ fn steady(port: u16, first: u64, count: u64, period: Duration, seconds: u64, win
});
}
+/// One client, COUNT messages of SIZE bytes over one stream, and the answer.
+fn drain_client(port: u16, count: u64, size: usize) {
+ let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
+ runtime.block_on(async move {
+ let mut endpoint = Endpoint::client(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
+ endpoint.set_default_client_config(client_config());
+ let connection = endpoint.connect(SocketAddr::from(([127, 0, 0, 1], port)), "localhost").unwrap().await.unwrap();
+ let (mut send, mut recv) = connection.open_bi().await.unwrap();
+ let message = vec![b'x'; size];
+ let began = Instant::now();
+ send.write_all(DRAIN).await.unwrap();
+ for _ in 0..count {
+ send.write_all(&(size as u32).to_le_bytes()).await.unwrap();
+ send.write_all(&message).await.unwrap();
+ }
+ send.write_all(&0u32.to_le_bytes()).await.unwrap();
+ send.finish().unwrap();
+ let answer = recv.read_to_end(16).await.unwrap();
+ let seconds = began.elapsed().as_secs_f64();
+ let read = u64::from_le_bytes(answer[..8].try_into().unwrap());
+ println!("{{\"messages\":{count},\"size\":{size},\"read\":{read},\"seconds\":{seconds:.3},\"per_second\":{:.0}}}",
+ count as f64 / seconds);
+ });
+}
+
fn main() {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("serve") if args.len() == 4 || args.len() == 5 => serve(std::fs::read(&args[2]).unwrap(),
std::fs::read(&args[3]).unwrap(),
args.get(4).map_or(0, |p| p.parse().unwrap())),
+ Some("drain") if args.len() == 5 => drain_client(args[2].parse().unwrap(), args[3].parse().unwrap(),
+ args[4].parse().unwrap()),
Some("steady") if args.len() == 8 => steady(args[2].parse().unwrap(), args[3].parse().unwrap(),
args[4].parse().unwrap(),
Duration::from_millis(args[5].parse().unwrap()),
@@ -322,7 +389,7 @@ fn main() {
args[4].parse().unwrap(), args[5].parse().unwrap()),
_ => {
eprintln!("idle-quic serve CERT.der KEY.der [PORT] | idle-quic connect PORT FIRST COUNT WINDOW | \
- idle-quic steady PORT FIRST COUNT PERIOD_MS SECONDS WINDOW");
+ idle-quic steady PORT FIRST COUNT PERIOD_MS SECONDS WINDOW | idle-quic drain PORT COUNT SIZE");
std::process::exit(2);
}
}
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)