diff options
9 files changed, 341 insertions, 30 deletions
diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md index d27aa5a..a49d702 100644 --- a/docs/design/011-pure-transport.md +++ b/docs/design/011-pure-transport.md @@ -1,6 +1,6 @@ # 011 — 연결 없는 전송: 전송의 상태를 앱의 데이터로 -작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1을 쟀다. +작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1, M2, M4를 쟀다. [외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. 1단계(링크 시제품과 kill -9 검사)는 [093](../research/093-connectionless-link.md)에 있다. 어느 쪽을 죽여도 핸드셰이크 없이 @@ -10,8 +10,12 @@ M1은 [094](../research/094-idle-links.md)에 있다. 쉬는 링크 하나가 노드 메모리 132 B를 쓴다. 허용 목록 73 B와 들은 번호 약 60 B이고, 전송 상태는 0이다. quinn의 쉬는 연결은 38,106 B다. 1/289로 중단 기준(1/100)을 넘는다. 설계가 말한 "링크의 -행"은 앱이 이미 가진 들은 번호였다. 그래서 들은 번호를 목록으로 바꿨고, 저장소의 한 항목은 약 20 B다. 남은 걸림돌은 M2다. -활성 상태를 비우면 비밀도 잊으므로, 재시작한 노드는 링크마다 X25519를 한 번 한다. +행"은 앱이 이미 가진 들은 번호였다. 그래서 들은 번호를 목록으로 바꿨고, 저장소의 한 항목은 약 20 B다. + +M2와 M4는 [095](../research/095-restart-and-first-contact.md)에 있다. 비밀은 노드 키 옆 파일에 적어 둔다(메모). 기기 1만 +개가 초당 2,000건을 보내는 중에 받는 쪽을 kill -9 해도 저장이 멈춘 100 ms 칸이 없다. 0.2초 만에 따라잡고, 재시작에 비대칭 +연산이 들지 않으며, 잃거나 겹친 기록이 없다. quinn은 1초 멈추고 10.9초 뒤에 따라잡는다. 그동안 핸드셰이크 1만 번에 서버가 CPU +약 7초를 더 쓴다. 처음 만남은 1 RTT로, QUIC의 2 RTT의 절반이다. ## 첫 판을 버린 까닭 diff --git a/docs/research/094-idle-links.md b/docs/research/094-idle-links.md index 57fc736..573db98 100644 --- a/docs/research/094-idle-links.md +++ b/docs/research/094-idle-links.md @@ -133,8 +133,8 @@ H1의 뒷부분("프로세스 메모리는 활성 링크 수에만 비례")은 � ## 한계와 다음 - **재시작의 비대칭 연산.** 활성 상태를 비우면 비밀도 잊는다. 그래서 재시작한 노드는 링크마다 X25519를 한 번 한다(169 µs). - 011의 M2 중단 기준("이어지는 데 링크 수에 비례하는 비대칭 연산")에 걸린다. 다음 일은 비밀의 메모다. 키 파일 옆 파일에 - 보내는 이마다 64 B 칸을 두고 매핑한다. 쉬는 링크의 칸은 상주하지 않는다. + 011의 M2 중단 기준("이어지는 데 링크 수에 비례하는 비대칭 연산")에 걸린다. [095](095-restart-and-first-contact.md)에서 비밀의 + 메모로 풀었다. 키 파일 옆 파일에 보내는 이마다 64 B 칸을 두고 매핑한다. 쉬는 링크의 칸은 상주하지 않는다. - **받는 쪽의 처리량.** 새 보내는 이를 받는 속도는 초당 약 3,700이다. 노드 CPU의 대부분이 X25519다. - **보내는 쪽.** 링크마다 스레드와 소켓이 하나씩이다. 노드가 기기 100만 대에 보내는 방향은 아직 안 된다. 보내는 쪽도 한 스레드와 한 소켓에 여러 링크를 둬야 한다. diff --git a/docs/research/095-restart-and-first-contact.md b/docs/research/095-restart-and-first-contact.md new file mode 100644 index 0000000..eb0249d --- /dev/null +++ b/docs/research/095-restart-and-first-contact.md @@ -0,0 +1,131 @@ +# 095 — 재시작과 처음 만남: 링크 1만 개가 kill -9를 모른다 + +2026-09-28. [설계 011](../design/011-pure-transport.md)의 M2와 M4다. [094](094-idle-links.md)의 부하 생성기와 quinn +하네스를 그대로 쓴다. + +착수 기준: + +- **바꿀 결정.** 011의 H2다. 어느 노드를 kill -9 해도 다시 뜬 뒤 첫 패킷으로 이어지고, 핸드셰이크도 잃거나 겹치는 기록도 없다. + 중단 기준은 둘이다. + - 재시작 뒤 핸드셰이크 없이 이어지지 않는다. + - 이어지는 데 링크 수에 비례하는 비대칭 연산이 든다. + - 둘 중 하나라도 그러면 다시 설계한다. +- **최소 확인.** + - M2: 기기 1만 개가 5초마다 주문 하나를 보낸다. 받는 쪽이 초당 2,000건을 받는다. 20초쯤에 받는 쪽을 kill -9 하고 곧바로 + 다시 띄운다. 저장이 멈춘 시간, 밀린 것을 따라잡는 시간, 양쪽 CPU, 비대칭 연산 수, 잃음과 겹침을 본다. quinn도 같은 부하로 + 한다. 연결 1만 개가 다시 맺어진다. + - M4: 처음 보는 상대에게 주문 하나를 보내 저장됐다는 말을 받기까지 걸리는 왕복 수를 본다. +- **중단 기준.** 위와 같다. + +## 쉬운 말로 + +- 알아낸 것: + - 받는 노드를 kill -9 하고 다시 띄워도 기기 1만 개는 거의 모른다. 100 ms 단위로 저장이 멈춘 때가 없었다. 밀린 것은 0.2초 + 만에 따라잡았다. 다시 보낸 주문은 34건이고, 8만 건이 모두 정확히 한 번 적용됐다. 재시작한 노드의 비대칭 연산은 0이다. + - quinn은 같은 일에서 1초 동안 멈췄다. 연결 1만 개를 다시 맺느라 10.9초 뒤에야 따라잡았다. 그동안 핸드셰이크에 서버가 + CPU를 약 7초, 클라이언트들이 약 8초 더 썼다. + - 처음 보는 상대와 Pure는 1 RTT에 끝나고, QUIC은 2 RTT가 든다. 첫 패킷이 이미 봉인된 기록이기 때문이다. +- 아직 모르는 것: 전달당 CPU와 처리량(M5), 손실과 병목 아래의 공정성(M6), 저장소 옮기기(M3). +- 사용 경험의 차이: 서버를 배포하거나 죽었다 살아나도, 붙어 있던 기기와 사용자는 모른다. 재연결 폭주도 없다. + +## 한 것 + +### 비밀의 메모 + +[094](094-idle-links.md)의 받는 쪽은 쉬는 보내는 이의 활성 상태를 비운다. 그러면 비밀도 잊는다. 그래서 재시작한 노드는 +돌아오는 보내는 이마다 X25519를 한 번 했다. 이 Pi의 OpenSSL로 169 µs다. 이것이 M2의 두 번째 중단 기준에 걸렸다. + +비밀은 이제 노드 키 옆의 파일(`KEY.links`, 0600)에 적어 둔다. + +- 보내는 이마다 허용한 순서대로 64 B 칸이 하나씩 있다. 칸에는 키의 앞 28 B, 검사값(키와 비밀의 FNV-1a), 비밀이 든다. +- 파일은 매핑한다. 쉬는 보내는 이의 칸은 메모리에 올라와 있지 않다. +- 비밀을 먼저 쓰고, 칸을 그 키의 것으로 만드는 키는 나중에 쓴다. 검사값이 맞지 않는 칸은 다시 만든다. 손으로 한 칸의 비밀을 + 바꿔 봤고, 그 칸만 X25519로 다시 만들어졌다. +- 노드 키가 바뀌면 파일을 처음부터 쓴다. +- 칸의 비밀은 노드 키와 같은 무게다. 그래서 키 파일과 같이 주인만 읽는다. + +연결 없는 모델에서는 이것이 자연스럽다. 비밀은 두 정적 키의 함수라서 언제 계산해도 같다. 메모는 그 함수의 기록일 뿐이다. +세션이 아니므로 틀려도 다시 계산하면 된다. + +### 꾸준히 보내는 기기들 + +부하 생성기에 `run`을 더했다([links.c](../../research/experiments/019-idle-links/links.c)). + +- 기기마다 소켓, 키, outbox가 있다. 주기마다 기록을 하나 만든다. +- 저장되지 않은 것은 다시 보낸다. 답이 없으면 200 ms에서 시작해 1초까지 기다림을 늘린다. +- 받는 쪽이 새로 뜨면 답의 run이 바뀐다. 기기는 그 run의 키를 만들어 연다. 기기 쪽의 일은 그것뿐이다. +- 100 ms마다 저장된 수를 센다. + +### quinn 쪽 + +QUIC에 가장 좋은 조건을 줬다. + +- 서비스는 같은 포트에 같은 키로 다시 뜬다. 상태 없는 리셋 키도 그 키에서 만든다. 그래서 새 서비스가 옛 연결의 패킷에 + 곧바로 리셋을 보낸다. +- 클라이언트는 1초 안에 답이 없거나 연결이 끊기면 다시 연결해 같은 주문을 보낸다. +- 서비스는 디스크에 저장하지 않는다. 메모리의 번호만 적는다. 그래서 정확히 한 번은 Pure 쪽만 확인한다. +- tokio 한 스레드다. Pure의 받는 쪽도 암호는 한 스레드(리스너)가 한다. + +## 결과 + +라즈베리 파이 5이고, 저장소는 tmpfs다(SD 카드는 따로 적었다). 원시 결과는 `research/experiments/019-idle-links/`의 +`2026-09-28-restart-*.jsonl`과 `2026-09-28-first-contact-linux-aarch64.jsonl`에 있다. + +**M2.** 기기 또는 클라이언트 1만 개가 5초마다 주문 하나를 보낸다(초당 2,000건). 받는 쪽을 약 21초에 kill -9 하고 곧바로 +다시 띄웠다. 세 라운드의 중앙값이다. SD 카드는 한 라운드다. + +| | 다시 뜨기 | 저장이 멈춘 100 ms 칸 | 따라잡기 | 받는 쪽 CPU(재시작 뒤 10초) | 비대칭 연산 | 보내는 쪽 CPU(같은 10초) | 다시 보냄 | 저장까지 p99 / 최대 | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| Pure, 메모 | 14 ms | 0 | 0.2초 | 1.08초 | 0 | 0.99초 | 34건 | 4.1 ms / 206 ms | +| Pure, 메모 없음 | 15 ms | 0 | 0.3초 | 2.73초 | X25519 10,000 | 0.86초 | 34건 | 4.6 ms / 221 ms | +| Pure, 메모, SD 카드 | 29 ms | 2 | 0.3초 | 1.06초 | 0 | 0.94초 | 269건 | 111 ms / 267 ms | +| quinn | 1.5 ms | 10 | 10.9초 | 8.30초 | 핸드셰이크 10,115 | 9.14초 | 연결 10,115개 | 5.3초 / 11.2초 | + +- 평소에는 받는 쪽도 보내는 쪽도 초당 약 0.1초의 CPU를 쓴다. 재시작 뒤 10초는 그 10배 안팎이 평소 몫이다. + - Pure는 메모가 있으면 재시작 뒤 CPU가 평소와 같다. + - 메모가 없으면 1.6초 더 든다. X25519 1만 번(각 0.165 ms)의 값이다. + - quinn은 받는 쪽이 7.2초, 보내는 쪽이 8.0초 더 쓴다. 우리 클라이언트는 인증서를 확인하지 않으므로, 실제 클라이언트는 더 + 든다. +- Pure의 모든 라운드에서 만든 주문, 저장됐다는 말을 받은 주문, 받는 앱이 적용한 주문의 수가 같았다(8만여 건). 잃거나 겹친 것이 + 없다. +- Pure의 기기는 재시작을 알아채지 않는다. 답이 오지 않은 주문을 다시 보낼 뿐이다. 1만 개 가운데 그때 답을 기다리던 30여 개다. +- SD 카드에서는 저장마다 동기화가 느려 p50이 4.1 ms다. 재시작 중에 밀린 것이 조금 더 많았다(269건). 그래도 0.3초 만에 + 따라잡았다. +- quinn은 새 서비스가 옛 연결에 리셋을 보낼 수 있는데도, 첫 1초 동안 저장이 하나도 없었다. 클라이언트의 1초 시한과 같은 + 길이다. 리셋이 닿지 않은 연결이 시한을 기다렸을 것으로 보지만, 원인은 확인하지 않았다. 그 뒤 핸드셰이크가 서비스의 한 + 스레드를 채웠다. 스레드를 늘리면 따라잡는 시간은 줄지만, 핸드셰이크 1만 번의 CPU는 그대로다. 서버는 핸드셰이크마다 X25519 + 둘(임시 키와 합의)과 ECDSA 서명 하나를 한다. + +**M4.** 처음 보는 상대 100개에게 하나씩 주문 하나를 보내고, 저장됐다는 말(quinn은 답)을 받기까지를 쟀다. 비특권 네트워크 +이름공간에서 루프백의 UDP만 netem으로 늦췄다. + +| 한쪽 지연 | RTT | Pure p50 | Pure 왕복 | quinn p50 | quinn 왕복 | +|---:|---:|---:|---:|---:|---:| +| 5 ms | 10 ms | 10.6 ms | 1.06 | 21.0 ms | 2.10 | +| 25 ms | 50 ms | 50.9 ms | 1.02 | 101.2 ms | 2.02 | +| 50 ms | 100 ms | 100.7 ms | 1.01 | 201.2 ms | 2.01 | + +- Pure는 첫 패킷이 이미 봉인된 기록이다. 받는 쪽이 저장하고 답하면 끝이라 1 RTT다. 받는 쪽의 X25519와 저장은 1 ms 안쪽이다. +- QUIC은 핸드셰이크에 1 RTT, 주문에 1 RTT가 든다. 세션 티켓의 0-RTT는 다시 만날 때만 되고, 재전송 공격에 열려 있다. + 재시작한 서비스는 티켓 키를 저장해 두지 않는 한 티켓을 받지 못한다. + +## 판정 + +- **M2는 통과다.** 받는 쪽을 kill -9 해도 핸드셰이크 없이 이어진다. 메모가 있으면 재시작에 비대칭 연산이 들지 않는다. 기록을 + 잃거나 겹치지 않았다. 011의 두 중단 기준 모두에 걸리지 않는다. +- **M4는 1 RTT다.** QUIC의 절반이다. 봉인한 첫 패킷이 기록이기 때문이다. 011이 비교 대상으로 적은 QUIC의 1-RTT는 핸드셰이크만의 + 값이고, 기록까지는 2 RTT다. +- H2가 섰다. 093은 정확성을, 여기서는 값을 봤다. 재시작의 값은 연결 없는 모델에서 거의 0이다. 연결이 있는 모델에서는 연결 수에 + 비례한다. + +## 한계와 다음 + +- **처음 만남은 여전히 X25519 한 번이다.** 새 보내는 이를 받는 속도는 초당 약 3,700이다(094). 쓰레기 패킷으로 이것을 강요하는 + 공격은 메모로 알려진 키에 대해서는 사라졌다. 모르는 run의 첫 패킷마다 드는 HKDF 두 번은 남는다. 비율 제한은 보안 단계에서 + 둔다. +- **메모는 허용 순서에 묶인다.** 허용 목록의 순서가 바뀌면 칸이 맞지 않아 다시 계산한다. 정확성에는 영향이 없다. +- **QUIC 비교는 quinn 하나다.** ngtcp2는 아직이다. +- **보내는 쪽.** 링크마다 스레드 하나다(094). +- 다음은 011의 순서대로다. + - 에폭 바꿈(전방 비밀성), 경로 바뀜, 저장소 옮기기(H3, M3), 보안 검사를 한다. + - 그 뒤 M5와 M6 가드레일을 잰다. 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) |
