aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHECKPOINT.md1
-rw-r--r--docs/design/011-pure-transport.md27
-rw-r--r--docs/design/012-link-protocol.md24
-rw-r--r--docs/research/102-adoption-and-spec.md253
-rw-r--r--research/experiments/020-lossy-paths/2026-09-29-paths-ngtcp2-linux-aarch64.jsonl124
5 files changed, 411 insertions, 18 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 91d8e7f..e6e9fa7 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -17,6 +17,7 @@
| 인터페이스 | 한 명세가 TUI(셀 단위 갱신), CLI, 문장(NUI), 웹 GUI가 된다. 저장하면 실행 중인 앱이 상태를 이어 새 모듈로 바뀐다(`aup watch`, `vega … --watch`) | Vega `docs/design/001-first-component.md`, Vega `make check`(가상 터미널, 헤드리스 Chromium 포함) |
| HTTP 서비스 | 보기가 GET, 입력이 POST다(JSON 본문과 그 안의 그룹 객체를 제자리에서 읽는다, 라우터 코드 없음). 앱의 `send`는 이벤트와 함께 저장되고, `--send 출력=URL`로 다른 앱이나 HTTP 끝점에 순서대로 묶어 전해지며, 받는 앱은 번호마다 한 번 적용한다(서버를 죽였다 살려도 정확히 한 번). `--seal 출력=공개키`면 받는 노드의 키로 봉인해(HPKE, 전달선마다 세션 하나, 보내는 노드의 키로 인증) 사이의 중계가 읽지도 바꾸지도 못하고, 받는 노드는 `--sender 이름=키`로 보낸 이를 가린다(봉인 전달 462k/s, 봉인 없이 468k/s). 답은 저장 뒤에 나간다. 상태를 가진 앱은 소유자 스레드 하나가 입력을 적용하고, 연결 스레드는 마지막 저장의 스냅숏으로 보기를 답한다(쓰기가 동기화하는 동안에도 읽기는 기다리지 않는다) | 008 HTTP 호스트, [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [082](docs/research/082-json-in-place.md), [083](docs/research/083-delivery-over-http.md), [086](docs/research/086-sealed-sends.md), [088](docs/research/088-sealed-sessions.md) |
| 성능(Pi 5) | 2026-09-27, 같은 기기에서 axum·drogon과 함께 쟀다. 문서 서버(한 스레드): 64 B GET 139k/s(CPU 1초당 140k; axum 127k·63k, drogon 147k·63k), 1 MiB GET 2,362/s(axum 1,304, drogon 754), 충돌 안전 PUT은 axum-atomic의 5.1–5.8배. 앱 서비스: 내구성 있는 쓰기 64연결 21.3k/s(axum·drogon 내구 약 200/s), 합계 GET 126k/s·두 스레드 153k(axum 136k, drogon 151k), 20행 페이지 두 스레드 148k(axum 93k, drogon 138k), 쓰기와 섞인 읽기 119k/s(axum·drogon 내구 1.1–1.4k/s). 유휴 RSS 2.3 MiB(axum 2.8, drogon 10.9), 최대 3.2–5.8 MiB(axum 6.6–9.7, drogon 11.9–78.8) | [075](docs/research/075-http-host-against-axum.md), [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [081](docs/research/081-tail-records.md), [087](docs/research/087-small-reads-and-idle-memory.md) |
+| 노드 사이 전달 | 연결 없는 링크가 기본이다(`--send 출력=pure://호스트:포트/입력`, 2026-09-29 채택). 두 노드의 키로 첫 패킷부터 봉인해 보내고 핸드셰이크가 없다. 어느 쪽을 kill -9 해도, 받는 노드의 저장소를 다른 주소로 옮겨도 다음 패킷으로 이어지며 잃거나 겹친 기록이 없다(기기 1만 개가 초당 2,000건을 보내는 중에 저장이 멈춘 100 ms 칸 0). 쉬는 링크는 받는 노드 메모리 132 B(quinn 연결의 1/289). 에폭마다 키를 바꿔 전방 비밀성을 얻는다. 전달당 CPU는 1 KiB 이상에서 quinn·ngtcp2보다 25 % 넘게 적고 61 B에서 비긴다. 손실·재정렬·병목 경로(M6)에서 재정렬 아래 quinn·ngtcp2보다 많이 나르고, 무작위 손실 아래 같으며, 깨끗한 병목에서 틀의 바이트로 3–5 % 적다. 명세가 있고, 명세만 보고 쓴 파이썬 받는 쪽이 노드와 맞붙는다 | [설계 011](docs/design/011-pure-transport.md), [명세 012](docs/design/012-link-protocol.md), [093](docs/research/093-connectionless-link.md)–[102](docs/research/102-adoption-and-spec.md) |
| 표면과 메신저 | 사람을 위한 표면이 노드 안에서 돈다(`pure-serve --surface`): 마지막 저장의 스냅숏을 제자리에서 읽고, 친 키와 글자는 HTTP 요청과 같은 줄로 소유자에게 간다. Vega의 디스플레이·픽셀이 그 표면이다. 두 노드의 메신저(Vega `examples/messenger.aup`)는 서로에게 봉인·인증해 보내고, 한 줄이 상대 노드에 보이기까지 RAM 저장소에서 p50 0.79 ms·p99 1.1 ms, SD 카드에서 p50 3.5 ms(꼬리는 SD의 동기화) | [설계 010](docs/design/010-surfaces.md), [089](docs/research/089-messenger.md) |
| 표 크기의 상태 | 목록이 영속 B+트리다(변경·키 탐색·저장본 위치 O(log n), 스냅숏 O(1), 1,600만 항목, 런타임 상태 1 GiB). pgbench의 TPC-B 같은 이체(세 표의 행과 이력, 한 트랜잭션)가 10만 계좌에서 RAM 24.5k/s(전: 3만 계좌 3.2k/s), SD 카드 내구 1,339/s(SQLite WAL FULL 84–153/s) | [090](docs/research/090-tables.md) |
| 일반 트랜잭션 | TPC-C의 다섯 트랜잭션을 aup 앱 하나로 쓴다(여러 표 읽기·쓰기, 조각 훑기, 중복 없는 셈, 실패하면 통째 되돌림). 같은 입력에서 SQLite와 아홉 표의 모든 행이 같다. W=1에서 RAM 초당 39,977건(SQLite FULL 9,529, 트랜잭션당 CPU 44–49 µs, HTTP·JSON 포함), SD 카드 내구 4,502건(SQLite FULL 50, NORMAL 372) | [092](docs/research/092-tpcc.md) |
diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md
index 5513476..392210c 100644
--- a/docs/design/011-pure-transport.md
+++ b/docs/design/011-pure-transport.md
@@ -56,18 +56,25 @@ M3(저장소 옮기기, H3)는 [100](../research/100-moved-receiver.md)에 있�
- 잃음·겹침 검사, 논스 검사, 보안 검사를 모두 통과한다.
- 전달당 CPU는 1 KiB 이상에서 두 QUIC보다 25 % 넘게 가볍다. 61 B에서는 ngtcp2의 1.04배로 10 % 안이다.
- TCP와의 몫은 ±25 % 안이다.
-- 전송 코드는 C 3,097줄이다.
+- 전송 코드는 C 3,146줄이다.
그래서 연결 없는 링크를 노드 사이 전달의 기본으로 삼는다(`--send OUTPUT=pure://host:port/INPUT`). HTTP 전달은 제3자와
-브라우저를 잇는 경계로 남고, 중계를 지나는 봉인한 전달([088](../research/088-sealed-sessions.md))도 그대로다. M6를 ngtcp2와
-재지는 않았다. 가드레일은 quinn과 TCP로 쟀다.
-
-명세 [012](012-link-protocol.md)를 쓰면서 구현의 구멍 셋을 찾아 고쳤다.
-- 받는 쪽이 받지 않는 스트림을 보내는 쪽은 아무 말도 듣지 못해 매초 다시 보냈다. 이제 REFUSED로 이유를 듣고 드물게 다시 시도한다.
-- 받는 쪽이 쥘 수 없는 16–64 MiB 기록이 영원히 다시 갔다.
-- 한 노드의 출력 셋 이상이 같은 노드로 가면 run들이 받는 쪽의 두 자리를 서로 밀어냈다.
-
-명세만 보고 파이썬으로 따로 쓴 받는 쪽이 `pure-serve` 노드와 에폭 바꿈까지 맞붙는다.
+브라우저를 잇는 경계로 남고, 중계를 지나는 봉인한 전달([088](../research/088-sealed-sessions.md))도 그대로다.
+
+채택과 함께 한 일은 [102](../research/102-adoption-and-spec.md)에 있다.
+- 명세 [012](012-link-protocol.md)를 쓰면서 구현의 구멍 넷을 찾아 고쳤다.
+ - 받는 쪽이 받지 않는 스트림을 보내는 쪽은 아무 말도 듣지 못해, 90초에 패킷 2만 2천여 개를 다시 보냈다. 이제 REFUSED로
+ 이유를 듣고 드물게 다시 시도한다(같은 90초에 9개).
+ - 받는 쪽이 쥘 수 없는 16–64 MiB 기록이 영원히 다시 갔다.
+ - 한 노드의 출력 셋 이상이 같은 노드로 가면 run들이 받는 쪽의 두 자리를 서로 밀어냈다.
+ - 메모가 피어마다 run 하나의 에폭만 들어, 다시 뜬 받는 쪽이 나머지 run에 LOST를 보냈다.
+- 명세만 보고 파이썬으로 따로 쓴 받는 쪽이 `pure-serve` 노드와 에폭 바꿈까지 맞붙는다.
+- 허가가 기록 1,024건으로 고정되어, 100 ms 경로에서 61 B 기록은 초당 1만 건이 끝이었다. 이제 받는 쪽 메모리에 맞추어 5만
+ 6천 건이다.
+- M6를 ngtcp2와도 쟀다.
+ - 재정렬만 있을 때 Pure가 가장 많이 나르고, 손실만 있을 때 셋이 같다.
+ - 깨끗한 병목에서는 틀의 바이트로 3–5 % 뒤진다.
+ - 손실과 재정렬이 겹칠 때 ngtcp2가 1.3–3.6배 나르는 것은, 잘못 본 손실을 되돌리며 진짜 손실의 감소까지 지우기 때문이다.
2026-09-29에 링크를 `link/`로 뗐다([009](009-layout.md)). 프로토콜은 이제 소켓, 스레드, 시계가 없는 상태이고
(`link/link-protocol.h`), UDP와 스레드는 그 위의 층이다. 그래서 한 스레드가 링크 여럿을 들 수 있다. M2를 이 보내는 쪽으로
diff --git a/docs/design/012-link-protocol.md b/docs/design/012-link-protocol.md
index 1ad3739..558ff7f 100644
--- a/docs/design/012-link-protocol.md
+++ b/docs/design/012-link-protocol.md
@@ -18,8 +18,10 @@
- **보내는 쪽**: 한 노드의 출력 하나가 저장한 보내기를 다른 노드의 입력 하나로 보낸다. 시작할 때마다 **run**을 새로 뽑는다.
run은 0이 아닌 무작위 u64다.
- **받는 쪽**: 받아들일 보내는 쪽을 (source 이름, 공개 키) 목록으로 가진다.
-- **스트림**: source 이름과 input 이름의 쌍이다. 각 이름은 1–64 B다. 메시지의 shape(u32)가 함께 간다.
-- **기록**: 스트림 안에서 1부터 연속된 번호(u64)를 가진 바이트열이다. 0–16 MiB다.
+- **스트림**: source 이름과 input 이름의 쌍이다. 각 이름은 1–64 B다. 메시지의 shape(u32)가 함께 간다. 받는 쪽 입력의
+ shape와 같아야 한다. 링크에게 shape는 비교만 하는 값이고, 뜻은 모듈 형식([008](008-module-format.md))이 정한다.
+- **기록**: 스트림 안에서 1부터 연속된 번호(u64)를 가진 바이트열이다. 0–16 MiB다. 바이트는 보내는 앱이 저장한 그대로이고,
+ 링크는 읽지 않는다.
- **들은 번호**: 받는 쪽 앱이 적용하고 저장한 마지막 기록 번호다.
- **답**: 받는 쪽이 보내는 쪽의 run에 보내는 패킷이다.
@@ -197,8 +199,12 @@ STORED의 들은 번호 이하인 기록은 덜어 낸다. 보내는 앱은 그�
쌓인다(지역 선택). 그래서 꾸며 낸 패킷 폭주에 드는 HKDF에 상한이 있고, 열리는 보내는 쪽 천 개가 한꺼번에 깨어나도 토큰을
쓰지 않는다.
- run의 자리는 패킷이 열린 뒤에만 만든다. 위조한 패킷은 아무 자리도 밀어내지 못한다.
-- 이미 받은 패킷 번호(구간 32개까지 기억)의 패킷은 ACK만 다시 하고 프레임은 처리하지 않는다. 그보다 오래된 번호는 기록의
- 들은 번호와 STORED의 "가장 큰 값" 규칙이 막는다.
+- 이미 받은 패킷 번호(구간 32개까지 기억)의 패킷은 ACK만 다시 하고 프레임은 처리하지 않는다. 그보다 오래된 번호의 패킷은
+ 다시 처리해도 해가 없다.
+ - 기록은 들은 번호가 걸러 한 번만 적용된다.
+ - 지난 OFFER는 지금 에폭 다음이 아니라 받아들여지지 않는다.
+ - 지난 RESPONSE는 지금의 CHALLENGE와 맞지 않는다.
+ - 주소는 가장 큰 번호의 패킷만 옮긴다.
**주소.**
@@ -209,7 +215,7 @@ STORED의 들은 번호 이하인 기록은 덜어 낸다. 보내는 앱은 그�
**답.**
-- 확인을 부르는 패킷을 받은 뒤의 다음 차례에 답 패킷 하나에 다음을 넣는다.
+- 확인을 부르는 패킷을 받으면 25 ms 안에 답 패킷 하나에 다음을 넣어 보낸다. 보내는 쪽의 PTO가 이 상한을 기대한다.
- CHALLENGE(확인 전)
- ACCEPT(다음 에폭이 쓰이기 전)
- ACK: 가장 큰 번호를 받은 뒤 지난 시간과 구간들
@@ -284,7 +290,8 @@ LOST는 그 패킷의 번호를 담아 에폭 0의 키로 봉인한다. 답 run�
| 거절 뒤 기다림 | 1초부터 두 배, 64초까지 | 지역 선택 |
| 에폭을 잃었다고 볼 때 | 2초와 PTO 4번 | 지역 선택 |
| 받은 뒤 저장을 기다리는 시간 | 1초 | 지역 선택 |
-| RTT 첫값, ACK 지연 상한, PTO 상한 | 100 ms, 25 ms, 1초 | 지역 선택 |
+| ACK 지연 상한(받는 쪽이 지킨다) | 25 ms | 명세 |
+| RTT 첫값, PTO 상한 | 100 ms, 1초 | 지역 선택 |
| 혼잡 제어 | CUBIC(RFC 9438, C 0.4, β 0.7), 첫 창 10 데이터그램, 페이싱, 잘못 본 손실은 되돌림 | 지역 선택(표준 알고리즘만) |
| 재정렬 인내 | 패킷 3–256개, 시간 RTT/8부터 최소 RTT의 1/4씩 RTT까지 | 지역 선택 |
| 키 만들기 / 에폭 수락 / LOST | 초당 256 / 256(한꺼번에 8) / 4,096 | 지역 선택 |
@@ -326,8 +333,9 @@ LOST는 그 패킷의 번호를 담아 에폭 0의 키로 봉인한다. 답 run�
## 이 명세와 구현
- 구현은 `link/link-protocol.c`(상태, 소켓 없음)와 `link/link.c`(UDP, GSO/GRO, 스레드)다.
-- 전송 코드는 C 3,097줄이다(프로토콜 2,510, UDP 587). 011의 한도는 5,000줄이다.
-- 이 명세를 쓰면서 구현의 구멍 셋을 찾아 고쳤다. 셋 다 `link/test-link.c`에 검사가 있다.
+- 전송 코드는 C 3,146줄이다(프로토콜 2,559, UDP 587). 011의 한도는 5,000줄이다.
+- 이 명세를 쓰면서 구현의 구멍 셋을 찾아 고쳤다. 셋 다 `link/test-link.c`에 검사가 있다. 메모의 곁 자리와 허가의 크기까지
+ [102](../research/102-adoption-and-spec.md)에 있다.
- 받는 쪽이 스트림을 받지 않을 때 보내는 쪽이 아무 말도 듣지 못해 매초 기록을 다시 보냈다. REFUSED를 더했다.
- 보내는 쪽은 64 MiB까지의 기록을 받아 두었지만 받는 쪽은 16 MiB까지만 쥘 수 있었다. 그 사이 크기의 기록은 영원히 다시
갔다. 기록의 상한을 16 MiB로 맞췄다.
diff --git a/docs/research/102-adoption-and-spec.md b/docs/research/102-adoption-and-spec.md
new file mode 100644
index 0000000..b7e04db
--- /dev/null
+++ b/docs/research/102-adoption-and-spec.md
@@ -0,0 +1,253 @@
+# 102 — 채택과 명세: 명세가 찾은 구멍 넷, ngtcp2와의 M6
+
+2026-09-29. [설계 011](../design/011-pure-transport.md)의 5단계다. 링크를 노드 사이 전달의 기본으로 정하고, 다른 구현이 맞붙을
+수 있는 명세 [012](../design/012-link-protocol.md)를 쓴다.
+
+착수 기준:
+
+- **바꿀 결정.** 노드 사이 전달의 기본을 연결 없는 링크로 정한다. 명세를 문서로 둔다.
+- **최소 확인.**
+ - 명세의 바이트와 규칙이 코드와 같다. 명세만 보고 따로 쓴 받는 쪽이 실제 노드와 맞붙어야 한다.
+ - [101](101-remaining-limits.md)이 남긴 공백을 채운다. M6(손실, 재정렬, 병목)를 ngtcp2와도 잰다.
+- **중단 기준.**
+ - 독립 구현이 맞붙지 않으면, 명세나 코드를 고친 뒤에 채택한다.
+ - M6에서 ngtcp2보다 가드레일(011: 10 %)을 넘게 나쁘면 채택을 미룬다.
+
+## 쉬운 말로
+
+- 알아낸 것:
+ - **명세.** 012가 머리, 키 유도, 프레임 13종, 두 쪽의 규칙, 명세의 값과 지역 선택, 보안 성질을 적는다.
+ - 명세만 보고 파이썬으로 받는 쪽을 따로 썼다. `cryptography` 패키지만 쓰고 `link/`의 코드는 쓰지 않았다.
+ - 그 받는 쪽이 `pure-serve` 노드와 맞붙는다. 주문 200건이 모두 저장되고, 첫 패킷 하나 뒤로는 두 쪽이 따로 셈한 에폭 1로
+ 봉인된다.
+ - **명세를 쓰다가 구멍 넷을 찾았다.** 규칙을 문장으로 옮기니 코드가 말하지 않던 경우가 드러났다.
+ - 받는 쪽이 받지 않는 스트림(허용되지 않은 source, 없는 입력, 다른 shape)을 보내는 쪽은 아무 말도 듣지 못했다. 쌓인 기록을
+ 매초 끝없이 다시 보냈고, 운영자는 아무것도 보지 못했다. 이제 REFUSED로 이유를 듣고, 1초에서 64초까지 늘어나는 간격으로
+ 패킷 하나씩만 시도한다.
+ - 보내는 쪽은 64 MiB까지의 기록을 받았지만, 받는 쪽은 16 MiB까지만 쥘 수 있었다. 그 사이 크기의 기록은 영원히 다시 갔다.
+ - 받는 쪽은 피어마다 run 자리를 둘만 두었다. 한 노드의 출력 셋이 같은 노드로 가면 run들이 서로 자리를 밀어내, 답이 엉뚱한
+ run으로 갔다. 검사에서 run 키를 3번이 아니라 7번 만들었고, 저장까지 16 ms가 아니라 1,016 ms가 걸렸다.
+ - 메모는 피어마다 run 하나의 에폭만 들었다. 그 출력 셋이 에폭 1에 있을 때 받는 쪽이 다시 뜨면 둘이 LOST를 받고 에폭을 다시
+ 시작했다. 이제 LOST도 X25519도 없다.
+ - **허가.** 받는 쪽의 허가가 기록 1,024건으로 고정되어, 링크 하나가 RTT마다 1,024건 넘게 나를 수 없었다. 100 ms 경로에서
+ 61 B 기록은 초당 1만 건이 끝이었다(경로가 나를 수 있는 양의 1/9). 이제 허가를 받는 쪽 메모리에 맞추어, 같은 경로에서 초당
+ 5만 6천 건이다.
+ - **M6와 ngtcp2.** 세 구현을 같은 병목(50 Mbit/s), 같은 경로에서 번갈아 쟀다.
+ - 재정렬만 있을 때 Pure가 가장 많이 나른다(45 Mbit/s). ngtcp2는 29–47, quinn은 1–2다.
+ - 손실만 있을 때는 셋이 같다.
+ - 손실도 재정렬도 없을 때는 Pure가 3–5 % 적다. 전부 틀의 바이트다. Pure는 패킷마다 틀이 약 112 B, QUIC은 약 41 B다.
+ - 손실과 재정렬이 함께 있을 때는 ngtcp2가 1.3–3.6배 나른다. ngtcp2는 재정렬로 잘못 본 손실을 되돌리면서, 그 앞의 진짜 손실이
+ 줄인 창까지 함께 되돌린다. ngtcp2의 로그와 코드로 확인했다(아래). Pure는 한 에피소드의 손실이 모두 늦게 온 것으로 밝혀질
+ 때만 되돌린다.
+- 아직 모르는 것:
+ - 다른 사람이 명세만으로 만든 구현은 아직 없다. 파이썬 받는 쪽은 명세를 쓴 사람이 썼다. 다만 코드를 옮기지 않고 명세의
+ 문장대로 썼다.
+ - 보내는 쪽의 독립 구현은 없다. 파이썬 쪽은 받는 쪽이다.
+- 사용 경험의 차이:
+ - 잘못 배선한 링크는 보내는 노드의 로그 한 줄로 드러난다. HTTP 전달이 받는 쪽의 거절을 알리는 것과 같은 모양이다. 종료
+ 통계에도 `link_refused`와 `listener_refused`로 남는다.
+ - 한 노드의 여러 출력을 같은 노드로 보내도 된다. 받는 쪽이 다시 떠도 모두 그대로 이어진다.
+
+## 한 것
+
+### 명세 012
+
+- 012는 바이트 하나하나를 코드와 맞춰 가며 썼다.
+ - 머리 21 B: 판과 에폭, 키 id(FNV-1a), run, 패킷 번호.
+ - 키 유도: HKDF-SHA256의 salt와 info 문자열, 누가 from이고 누가 to인지, 어느 run이 들어가는지.
+ - 논스, 에폭의 사슬, 프레임 13종의 배치.
+- 판은 선 위에서 협상하지 않는다. 판은 받는 쪽의 키와 함께 배선에서 온다. 다음 판은 판 번호와 키 유도 문자열을 함께 바꾸므로,
+ 판이 다른 두 구현은 서로의 패킷을 열 수 없다.
+- 값은 둘로 나눴다. 상대가 알아야 하는 명세의 값(데이터그램, 허가, 증폭 한도, LOST의 크기 조건)과, 한쪽이 혼자 정해도 되는 지역
+ 선택(혼잡 제어, PTO, 비율 한도, 쉬는 시간)이다.
+- 선 위의 알림은 LOST와 REFUSED 둘이고, 둘 다 인증된다. 그 밖의 실패에는 답하지 않는다.
+
+### 독립 구현
+
+`tests/check.py`의 `server_links_independent`가 파이썬 받는 쪽이다. 명세의 다음 부분을 모두 거친다.
+
+- 정적 비밀(X25519의 32 B 그대로), 패킷 키(HKDF, info), 논스, 머리를 추가 인증 데이터로 쓰는 AES-128-GCM.
+- 프레임 읽기: STREAM, RECORDS, RECORD, OFFER, RESPONSE, PING, PADDING.
+- 답 만들기: CHALLENGE, ACCEPT(되풀이 4 B), ACK(구간), STORED. 답 run과 보내는 쪽 run을 넣은 답의 키.
+- 에폭 1의 비밀: 파이썬 쪽의 새 키 쌍과 노드의 OFFER로 E(1)을 따로 셈한다. 노드의 패킷 199개가 그 키로 열린다.
+- 새 주소의 확인(CHALLENGE, RESPONSE)과 증폭 한도(3배).
+
+다섯 번 돌려 모두 같았다. 0.8초에 끝난다.
+
+### REFUSED
+
+- **받는 쪽.** STREAM을 받지 않으면 이유와 함께 그 run의 다음 답에 REFUSED(0x0c)를 넣는다. 한 차례에 한 번이다.
+ - 이유 1: 그 이름의 메시지 입력이 없다.
+ - 이유 2: 입력의 shape가 다르다.
+ - 이유 3: 이 키는 그 source로 보낼 수 없다.
+ - 호스트의 입력 콜백이 1과 2를 가른다(`-PURE_LINK_OTHER_SHAPE`). 전의 `-1`은 그대로 1이다.
+- **보내는 쪽.** 기록을 보내지 않고 기다린다. 기다린 뒤 기록을 실은 패킷 하나로 시도한다.
+ - 기다림은 1초에서 시작해 두 배씩, 64초까지 는다.
+ - 그 패킷을 확인한 답에 REFUSED가 없으면 거절이 끝난다. STORED를 들어도 끝난다.
+- **처음 만든 판의 구멍.** 처음에는 STORED를 들어야만 거절을 풀었다. 그런데 시도한 패킷이 큰 기록의 앞 조각만 실으면, 받는
+ 쪽은 저장할 것이 없어 STORED를 보내지 않는다. 보내는 쪽은 STORED만 기다리며 멈췄다. 검사가 이것을 잡았다. 그래서 "시도한 패킷
+ 자체가 REFUSED 없이 확인됨"으로 푼다. PING만 든 탐침의 확인으로 잘못 풀지 않도록, 시도한 패킷의 번호가 확인됐는지를 본다.
+- **검사.**
+ - test-link: 세 가지 이유가 각각 90초에 7번 거절되고, 패킷은 9개다. 입력이 없다가 10초에 생긴 스트림은 그 뒤 첫 시도(15초)에
+ 풀려 200건이 모두 저장된다.
+ - check.py: 없는 입력으로 링크를 건 노드가 로그에 한 번 알리고, 2.5초 동안 패킷 2개만 보낸다.
+- REFUSED 전에는 같은 90초에 거절된 스트림 하나가 패킷 22,593–23,225개를 보냈고, 기록을 40,570–52,976번 다시 보냈다. 받았다고
+ 들은 기록이 1초 안에 저장되지 않으면 모두 다시 보내는 규칙이 끝없이 돌았다. 옛 커밋(78231dc)의 라이브러리에 같은 검사를
+ 붙여 쟀다.
+
+### 기록의 크기
+
+- 보내는 쪽의 상한(64 MiB)과 받는 쪽이 한 스트림에서 쥐는 양(16 MiB)이 달랐다. 16 MiB가 넘는 기록은 받는 쪽이 버리고, 보내는
+ 쪽은 저장 소식이 없으니 다시 보냈다.
+- 이제 둘 다 `PURE_LINK_RECORD_MOST`(16 MiB)다. 명세의 값이다. test-link가 16 MiB 기록 하나를 한 번에 보내고(가상 59 ms),
+ 1 B 더 큰 것은 큐에 받지 않는 것을 본다.
+
+### 피어의 run 자리
+
+- 받는 쪽은 피어마다 run 자리 둘을 두었다. 보내는 쪽의 run과, 그것이 다시 떴을 때의 새 run을 위한 것이었다.
+- 그런데 한 노드의 출력 여럿이 같은 노드로 가면, 출력마다 보내는 쪽이 따로 있고 run도 따로다. 셋이면 자리 둘을 서로 밀어낸다.
+ - 밀려난 run의 패킷은 키를 다시 만든다.
+ - 답 목록이 가리키던 자리를 다른 run이 차지해, 답이 엉뚱한 run으로 간다.
+- 이제 가장 오래 조용한 run도 아직 살아 있으면(쉬는 시간 안에 보냈으면) 자리를 하나 더 만든다. 피어당 64개까지다. 자리는
+ 피어가 쉬어 놓일 때까지 그대로 두므로, 가리키는 것들이 늘 맞다.
+- test-link: 한 키로 세 출력이 1,000건씩 보낸다. 고치기 전에는 run 키를 7번 만들었고, 저장까지 가상 1,016 ms가 걸렸다. 고친
+ 뒤에는 3번, 16 ms다.
+
+### 메모의 곁 자리
+
+- 메모는 피어의 자리에 run 하나의 에폭만 두었다. 여러 run이 함께 보내는 피어는 받는 쪽이 다시 뜬 뒤 마지막으로 받아들인 run
+ 말고는 모두 LOST를 받았다. 에폭 0으로 돌아가므로, 기록이 한 왕복 동안 정적 키 아래로 가고 X25519가 더 든다.
+- 이제 피어의 자리에 든 run이 아직 보내는 동안, 다른 run의 에폭은 곁의 자리에 둔다.
+ - 곁 자리는 1,024개이고 파일 머리에 있다(160 KiB). 피어와 run으로 두 자리를 찾아, 가진 것, 빈 것, 첫째 순으로 고른다.
+ - run이 피어의 자리로 옮겨 가면 곁 자리를 지운다. 에폭의 비밀은 다음 에폭이 덮을 때까지만 둔다는 규칙을 지키려는 것이다.
+ - 형식이 바뀌어 메모는 PLM3이다. PLM2 메모는 처음부터 다시 만든다(피어마다 X25519 한 번).
+- test-link: 세 출력이 모두 에폭 1에 든 뒤 받는 쪽을 kill -9처럼 다시 띄운다. 고치기 전에는 둘이 LOST를 받고 수락 한 번(비밀
+ 2개)이 들었다. 고친 뒤에는 둘 다 0이다.
+
+### 허가를 기록의 크기에 맞춤
+
+M6의 100 ms 경로에서 Pure가 20 ms 때보다 패킷을 28 % 더 보냈다(107,968 대 84,106). goodput은 거의 같았고 손실은 0이었다.
+패킷이 덜 찼다는 뜻이다.
+
+- **원인.** 받는 쪽의 허가가 기록 1,024건으로 고정되어 있었다.
+ - 100 ms에 50 Mbit/s이면 대역폭×지연과 병목 큐에 든 기록이 1,024건을 넘는다.
+ - 그래서 보내는 쪽은 STORED가 조금씩 연 만큼만, 한두 건씩 든 패킷을 보냈다.
+- **더 큰 한계.** 링크 하나가 RTT마다 1,024건을 넘게 나를 수 없었다. 시뮬레이터(가상 시간, 실제 프로토콜 코드)로 쟀다.
+
+ | 기록 | RTT 10 ms | RTT 100 ms |
+ |---|---:|---:|
+ | 61 B | 90,814건/s(44 Mbit/s, 대역폭에 걸림) | 9,953건/s(4.9 Mbit/s) |
+ | 1 KiB, 보낸 패킷 | 74,890 | 99,477 |
+
+ 61 B 기록은 100 ms 경로에서 대역폭과 상관없이 초당 1만 건이 상한이었다. 경로가 나를 수 있는 양의 1/9이다.
+- **고침.** 받는 쪽은 메모리 한도(스트림당 16 MiB)를 따로 지키고 있다. 그러니 허가를 건수로 고정할 까닭이 없다.
+ - 허가는 남은 자리를 지난 전달의 기록당 바이트로 나눈 것이다. 1–65,536건이고, 전달이 없으면 전처럼 1,024건이다.
+ - 받는 범위는 들은 번호 + 65,536까지다. 보내는 쪽이 따르는 상한과 같다.
+ - 메모리를 넘는 기록은 전처럼 받지 않고, 보내는 쪽이 다시 보낸다.
+- **고친 뒤.**
+
+ | | 전 | 후 |
+ |---|---:|---:|
+ | 시뮬레이터, 61 B, RTT 100 ms | 9,953건/s | 56,296건/s |
+ | 시뮬레이터, 61 B, RTT 10 ms | 90,814건/s, 손실 0 | 87,130건/s, 손실 1,371 |
+ | 시뮬레이터, 1 KiB, RTT 100 ms | 44.1 Mbit/s, 패킷 99,477 | 44.0 Mbit/s, 패킷 75,938 |
+ | M6, 1 KiB, RTT 100 ms, 손실 0 | 44.9 Mbit/s, 패킷 107,968 | 45.4 Mbit/s, 패킷 83,566 |
+
+ - 이제는 허가가 아니라 혼잡 창이 병목의 큐를 채운다. 그래서 큐에서 버려지는 손실이 생긴다. 전에는 허가가 뜻하지 않게 창을
+ 대신 묶어 그것을 가렸다.
+
+### M6: ngtcp2
+
+- 101의 [drain.c](../../research/experiments/019-idle-links/ngtcp2/drain.c)에 quinn 하네스와 같은 두 모드를 더했다.
+ - 속도 맞춤: 초당 RATE개, 각 메시지 앞 8 B에 쓴 시각을 넣는다.
+ - 시간 제한: SECONDS가 지나면 더 쓰지 않는다.
+ - 받는 쪽은 quinn과 같은 줄을 낸다. 100 ms마다 읽은 메시지 수와, 속도를 맞췄을 때 걸린 시간이다.
+- ngtcp2는 스트림 바이트를 확인될 때까지 앱이 들고 있어야 한다. 속도 맞춤 모드는 모든 메시지 자리를 미리 잡아 두고, 제 시각에
+ 시각을 찍어 스트림의 끝을 늘린다.
+- 020의 run.py가 ngtcp2를 세 번째로 돌린다. 같은 병목 뒤의 포트 40005다. 경우마다 Pure, quinn, ngtcp2를 번갈아 돌린다.
+- **하네스의 구멍.** 처음 돌렸을 때 재정렬이 있는 경우에서 ngtcp2가 멈췄다.
+ - OpenSSL 3.5의 ClientHello는 기본으로 ML-KEM 혼합 키를 실어 Initial 패킷 둘로 나뉜다.
+ - 경로가 둘째를 먼저 가져다주면 ngtcp2의 서버는 Retry를 요구하는데, 하네스가 그것을 처리하지 않았다.
+ - 클라이언트가 X25519만 내게 했다. Pure의 링크도 X25519로 합의한다. 서버는 붙들 수 없는 연결을 놓고, 클라이언트의 다음
+ Initial로 새로 시작한다.
+
+## 결과
+
+**M6** (두 라운드, 경우마다 세 구현을 번갈아, 라운드마다의 값;
+`research/experiments/020-lossy-paths/2026-09-29-paths-ngtcp2-linux-aarch64.jsonl`). Pure의 값은 허가를 고치기 전이다. 고친
+뒤 100 ms 행은 45.4, 45.4, 1.5, 1.4, 0.5, 0.6 Mbit/s이고, 나머지 행은 잡음 안에서 같다.
+
+대용량(1 KiB 기록, Mbit/s):
+
+| RTT | 손실 | 흔들림 | Pure | quinn | ngtcp2 |
+|---:|---:|---:|---|---|---|
+| 20 | 0 % | 0 ms | 45.4 / 45.4 | 46.8 / 46.8 | 47.2 / 47.2 |
+| 20 | 0 % | 3 ms | 45.4 / 45.4 | 1.9 / 2.1 | 40.0 / 39.8 |
+| 20 | 1 % | 0 ms | 6.6 / 7.0 | 6.9 / 7.6 | 7.2 / 7.5 |
+| 20 | 1 % | 3 ms | 7.0 / 7.6 | 1.6 / 1.8 | 25.5 / 26.2 |
+| 20 | 5 % | 0 ms | 2.8 / 3.0 | 2.8 / 3.1 | 3.0 / 3.3 |
+| 20 | 5 % | 3 ms | 3.1 / 3.1 | 1.7 / 1.4 | 6.0 / 9.3 |
+| 100 | 0 % | 0 ms | 44.9 / 44.9 | 47.3 / 47.3 | 47.2 / 47.2 |
+| 100 | 0 % | 3 ms | 45.2 / 45.2 | 1.0 / 1.3 | 29.1 / 47.2 |
+| 100 | 1 % | 0 ms | 1.3 / 1.6 | 1.6 / 1.3 | 1.7 / 1.2 |
+| 100 | 1 % | 3 ms | 1.4 / 1.4 | 0.8 / 0.8 | 1.8 / 2.5 |
+| 100 | 5 % | 0 ms | 0.6 / 0.6 | 0.6 / 0.5 | 0.6 / 0.7 |
+| 100 | 5 % | 3 ms | 0.6 / 0.7 | 0.4 / 0.4 | 1.2 / 0.9 |
+
+속도를 맞춘 전송(초당 50건, 쓴 때부터 받는 쪽이 넘길 때까지 p50 / p99 / 최대 ms, 두 라운드의 중앙값):
+
+| RTT | 손실 | Pure | quinn | ngtcp2 |
+|---:|---:|---|---|---|
+| 20 | 1 % | 10.0 / 50.1 / 60.2 | 10.1 / 50.3 / 70.4 | 10.0 / 40.1 / 53.2 |
+| 20 | 5 % | 10.0 / 61.7 / 110.1 | 10.1 / 84.0 / 122.6 | 10.0 / 71.2 / 110.6 |
+| 100 | 1 % | 50.0 / 130.1 / 230.1 | 50.1 / 180.3 / 236.3 | 50.0 / 173.7 / 210.5 |
+| 100 | 5 % | 60.2 / 411.5 / 467.9 | 70.7 / 406.3 / 490.5 | 71.9 / 292.0 / 366.8 |
+
+TCP CUBIC 옆에서의 몫(흔들림 1 ms, 라운드마다): 20 ms에서 Pure 0.53, 0.47, ngtcp2 0.52, 0.47, TCP끼리 0.53, 0.50이다.
+100 ms에서 Pure 0.57, 0.58, ngtcp2 0.61, 0.56, TCP끼리 0.47, 0.49다. quinn은 흔들림 1 ms에도 무너져 0.02–0.07이다.
+
+**ngtcp2가 손실과 재정렬 아래에서 더 나르는 까닭.** 무작위 손실 1 %, RTT 20 ms에서 Reno 계열이 낼 수 있는 이론값(Mathis 식)은
+약 7 Mbit/s다. 재정렬이 없을 때는 셋 다 거기에 있다. 재정렬이 더해지면 ngtcp2만 25 Mbit/s로 오른다.
+
+- ngtcp2의 CUBIC은 혼잡 사건 때 되돌릴 창을 저장한다. 그런데 지금 창이 저장된 값보다 클 때만 덮어쓰고, 잘못 본 손실을
+ 되돌릴 때에만 지운다(`lib/ngtcp2_cc.c`, 1.25.0의 478행과 551행).
+- 진짜 손실의 패킷은 PTO의 3배가 지나면 손실 목록에서 빠진다(`ngtcp2_conn_remove_lost_pkt`). 그 뒤에 재정렬로 잘못 본 손실
+ 하나가 늦게 확인되면, 목록의 손실이 모두 확인된 것으로 보고 저장된 창을 되살린다. 저장된 창은 그 앞의 진짜 손실이 줄이기
+ 전의 것이다.
+- 로그로 셌다(5초 × 2, 같은 경로).
+ - 재정렬이 없으면 창을 55번 줄이고 한 번도 되돌리지 않는다.
+ - 재정렬이 있으면 226번 줄이고 34번 되돌린다. 34번 가운데 22번은 창을 바로 앞 감소 전보다 크게 되돌렸다. 그 앞의 진짜 손실이
+ 줄인 창까지 지운 것이다.
+- 그러니 이 행의 차이는 손실에 반응하지 않은 몫이다. 경로를 함께 쓰는 흐름이 있으면 그만큼 덜 비킨다. Pure는 한 에피소드의
+ 손실이 모두 늦게 온 것으로 밝혀질 때만 되돌린다(097).
+
+**틀의 바이트.** 손실도 재정렬도 없는 병목에서 Pure는 ngtcp2보다 3–5 % 적게 나른다. 셈하면 전부 틀이다.
+
+- Pure의 가득 찬 1,452 B 패킷의 틀은 약 112 B다.
+ - 머리와 태그 37 B
+ - STREAM 25 B(이름 두 개)
+ - 앞 패킷에서 이어진 조각의 RECORD 19 B
+ - RECORDS 10 B와 크기 2 B
+ - 다음 패킷으로 넘어가는 조각의 RECORD 19 B
+- QUIC은 짧은 머리와 태그 약 27 B, STREAM 프레임 약 8 B, 메시지마다 길이 4 B로 약 41 B다.
+- 셈으로는 Pure가 ngtcp2의 0.95배를 나른다. 잰 값은 0.96배다.
+
+## 판정
+
+- 명세가 코드와 같다는 것을 독립 구현으로 보였다. 파이썬 받는 쪽이 에폭 바꿈까지 맞붙는다.
+- 명세를 쓰다가 찾은 구멍 넷과 M6가 드러낸 허가의 한계를 고쳤다. 각각 옛 코드에서 실패하고 새 코드에서 통과하는 검사가 있다.
+- M6에서 ngtcp2와 대었다. 재정렬 아래에서 Pure가 가장 많이 나르고, 손실만 있을 때 셋이 같다. 틀의 바이트로 3–5 % 뒤지고,
+ 손실과 재정렬이 겹칠 때 ngtcp2가 더 나르는 것은 손실에 덜 반응해서다. 011의 가드레일(TCP와의 몫 ±25 %)을 셋 다 지킨다
+ (quinn은 흔들림에 무너진다).
+- 채택 결정(011 5단계)은 그대로다.
+
+## 한계와 다음
+
+- 파이썬 받는 쪽은 명세를 쓴 사람이 썼다. 명세의 모호함은 다른 사람이 구현할 때 더 드러날 것이다.
+- 보내는 쪽의 독립 구현은 없다.
+- 틀의 바이트는 판 1에서 줄일 수 있다. 약 112 B를 약 45 B로 줄이면 QUIC과 비긴다.
+ - 받는 쪽이 그 run의 스트림을 안다는 것을 들은 뒤에는 STREAM을 뺀다(101에서 시도했던 설계, 25 B).
+ - 패킷의 기록 바이트를 조각이든 온전한 기록이든 한 프레임으로 잇는다(약 40 B).
+ - 패킷 번호를 줄여 싣는다(약 6 B).
+- 100 ms, 5 % 손실의 속도 맞춤 전송에서 p99는 ngtcp2가 낮다(292 대 412 ms). 라운드마다 크게 흔들리는 경우이고(097에서도
+ Pure 330–884 ms, quinn 310–752 ms), 원인은 아직 보지 않았다.
diff --git a/research/experiments/020-lossy-paths/2026-09-29-paths-ngtcp2-linux-aarch64.jsonl b/research/experiments/020-lossy-paths/2026-09-29-paths-ngtcp2-linux-aarch64.jsonl
new file mode 100644
index 0000000..0846980
--- /dev/null
+++ b/research/experiments/020-lossy-paths/2026-09-29-paths-ngtcp2-linux-aarch64.jsonl
@@ -0,0 +1,124 @@
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 45.39, "resent": 496, "lost": 248, "late": 0, "undone": 0, "packets": 84106}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 46.77, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 47.22, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 45.38, "resent": 429, "lost": 221, "late": 26, "undone": 9, "packets": 84066}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.91, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 39.96, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 6.63, "resent": 476, "lost": 238, "late": 0, "undone": 0, "packets": 13933}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 6.86, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 7.23, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 7.04, "resent": 319, "lost": 172, "late": 29, "undone": 17, "packets": 13421}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.65, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 25.48, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 2.75, "resent": 668, "lost": 314, "late": 0, "undone": 0, "packets": 5843}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 2.79, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 3.04, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 3.12, "resent": 659, "lost": 341, "late": 34, "undone": 17, "packets": 6130}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.66, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 5.98, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.042, "waited_ms_p99": 10.142, "waited_ms_max": 10.457, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1000}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.064, "waited_ms_p99": 10.174, "waited_ms_max": 10.287}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.4, "waited_ms_p50": 10.037, "waited_ms_p99": 10.116, "waited_ms_max": 10.355}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.039, "waited_ms_p99": 50.098, "waited_ms_max": 50.169, "resent": 19, "lost": 19, "late": 0, "undone": 0, "packets": 1019}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.069, "waited_ms_p99": 50.313, "waited_ms_max": 90.459}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.035, "waited_ms_p99": 50.021, "waited_ms_max": 55.54}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.041, "waited_ms_p99": 53.189, "waited_ms_max": 110.084, "resent": 52, "lost": 51, "late": 0, "undone": 0, "packets": 1052}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.075, "waited_ms_p99": 84.455, "waited_ms_max": 109.393}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.4, "waited_ms_p50": 10.038, "waited_ms_p99": 70.231, "waited_ms_max": 92.479}
+{"kind": "share", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 24.53, "tcp_mbit": 21.98, "share": 0.527}
+{"kind": "share", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 3.34, "tcp_mbit": 44.39, "share": 0.07}
+{"kind": "share", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 24.73, "tcp_mbit": 22.78, "share": 0.521}
+{"kind": "share", "who": "tcp", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 25.37, "tcp_mbit": 22.45, "share": 0.531}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 44.86, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 107968}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 47.28, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 47.22, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 45.23, "resent": 30, "lost": 16, "late": 16, "undone": 10, "packets": 85988}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.04, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 29.12, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 1.28, "resent": 76, "lost": 38, "late": 0, "undone": 0, "packets": 2791}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 1.58, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 1.66, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.38, "resent": 60, "lost": 34, "late": 7, "undone": 3, "packets": 3023}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 0.8, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.82, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.62, "resent": 151, "lost": 71, "late": 0, "undone": 0, "packets": 1362}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.58, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.63, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 0.6, "resent": 147, "lost": 75, "late": 4, "undone": 2, "packets": 1296}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 0.4, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.21, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.042, "waited_ms_p99": 50.203, "waited_ms_max": 50.964, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1000}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.092, "waited_ms_p99": 50.259, "waited_ms_max": 51.777}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.031, "waited_ms_p99": 50.242, "waited_ms_max": 53.073}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.044, "waited_ms_p99": 90.097, "waited_ms_max": 170.125, "resent": 2, "lost": 2, "late": 0, "undone": 0, "packets": 1002}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.065, "waited_ms_p99": 170.269, "waited_ms_max": 236.266}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.4, "waited_ms_p50": 50.036, "waited_ms_p99": 177.252, "waited_ms_max": 216.722}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 70.283, "waited_ms_p99": 551.387, "waited_ms_max": 604.378, "resent": 68, "lost": 47, "late": 0, "undone": 0, "packets": 945}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.4, "waited_ms_p50": 70.752, "waited_ms_p99": 406.514, "waited_ms_max": 494.708}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 69.515, "waited_ms_p99": 280.505, "waited_ms_max": 356.452}
+{"kind": "share", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 26.56, "tcp_mbit": 19.82, "share": 0.573}
+{"kind": "share", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 1.12, "tcp_mbit": 46.68, "share": 0.023}
+{"kind": "share", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 28.92, "tcp_mbit": 18.55, "share": 0.609}
+{"kind": "share", "who": "tcp", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 22.57, "tcp_mbit": 25.25, "share": 0.472}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 45.39, "resent": 498, "lost": 249, "late": 0, "undone": 0, "packets": 84101}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 46.77, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 47.22, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 45.39, "resent": 543, "lost": 278, "late": 22, "undone": 8, "packets": 84059}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 2.06, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 39.8, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 6.97, "resent": 265, "lost": 132, "late": 0, "undone": 0, "packets": 13623}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 7.56, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 7.51, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 7.6, "resent": 321, "lost": 167, "late": 28, "undone": 17, "packets": 14396}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.84, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 26.19, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 2.95, "resent": 645, "lost": 305, "late": 0, "undone": 0, "packets": 5977}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 3.11, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 3.26, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 3.11, "resent": 633, "lost": 332, "late": 34, "undone": 23, "packets": 6099}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.44, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 9.27, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.048, "waited_ms_p99": 10.126, "waited_ms_max": 10.395, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1000}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.079, "waited_ms_p99": 10.201, "waited_ms_max": 10.24}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.038, "waited_ms_p99": 10.113, "waited_ms_max": 10.287}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.042, "waited_ms_p99": 50.124, "waited_ms_max": 70.226, "resent": 15, "lost": 15, "late": 0, "undone": 0, "packets": 1016}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.078, "waited_ms_p99": 50.204, "waited_ms_max": 50.369}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.041, "waited_ms_p99": 30.176, "waited_ms_max": 50.812}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.051, "waited_ms_p99": 70.118, "waited_ms_max": 110.173, "resent": 53, "lost": 53, "late": 0, "undone": 0, "packets": 1053}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.079, "waited_ms_p99": 83.603, "waited_ms_max": 135.717}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.4, "waited_ms_p50": 10.041, "waited_ms_p99": 72.212, "waited_ms_max": 128.669}
+{"kind": "share", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 21.96, "tcp_mbit": 24.68, "share": 0.471}
+{"kind": "share", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 3.4, "tcp_mbit": 44.32, "share": 0.071}
+{"kind": "share", "who": "ngtcp2", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 22.56, "tcp_mbit": 24.98, "share": 0.475}
+{"kind": "share", "who": "tcp", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 24.11, "tcp_mbit": 23.71, "share": 0.504}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 44.86, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 108011}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 47.28, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 47.22, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 45.23, "resent": 31, "lost": 18, "late": 18, "undone": 11, "packets": 86033}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.32, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 47.22, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 1.63, "resent": 50, "lost": 25, "late": 0, "undone": 0, "packets": 3133}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 1.29, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 1.23, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.4, "resent": 68, "lost": 36, "late": 7, "undone": 3, "packets": 2959}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.85, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 2.53, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.62, "resent": 164, "lost": 76, "late": 0, "undone": 0, "packets": 1247}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.52, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.67, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.67, "resent": 129, "lost": 67, "late": 3, "undone": 2, "packets": 1321}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.43, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "bulk", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.9, "waited_ms_p50": 0.0, "waited_ms_p99": 0.0, "waited_ms_max": 0.0}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.049, "waited_ms_p99": 50.146, "waited_ms_max": 50.349, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1000}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.072, "waited_ms_p99": 50.206, "waited_ms_max": 52.868}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.037, "waited_ms_p99": 50.12, "waited_ms_max": 53.075}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.051, "waited_ms_p99": 170.163, "waited_ms_max": 290.147, "resent": 11, "lost": 11, "late": 0, "undone": 0, "packets": 1012}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.089, "waited_ms_p99": 190.321, "waited_ms_max": 236.309}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.4, "waited_ms_p50": 50.038, "waited_ms_p99": 170.241, "waited_ms_max": 204.279}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.07, "waited_ms_p99": 271.525, "waited_ms_max": 331.5, "resent": 44, "lost": 38, "late": 0, "undone": 0, "packets": 1002}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.4, "waited_ms_p50": 70.637, "waited_ms_p99": 406.149, "waited_ms_max": 486.327}
+{"kind": "paced", "who": "ngtcp2", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 74.304, "waited_ms_p99": 303.412, "waited_ms_max": 377.202}
+{"kind": "share", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 26.75, "tcp_mbit": 19.64, "share": 0.577}
+{"kind": "share", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 0.97, "tcp_mbit": 46.84, "share": 0.02}
+{"kind": "share", "who": "ngtcp2", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 26.65, "tcp_mbit": 20.8, "share": 0.562}
+{"kind": "share", "who": "tcp", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 23.57, "tcp_mbit": 24.25, "share": 0.493}