aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 11:49:32 +0100
committereunhhu <[email protected]>2026-09-29 11:49:32 +0100
commit8140a59205911170e151c95ed5596db7aa0d5ba5 (patch)
tree4ac10d2390ad4486f278c5b37e4a5e5966b93657
parent34fc79fe568784294a274851e3040c1171af1fe2 (diff)
downloadpure-8140a59205911170e151c95ed5596db7aa0d5ba5.tar.gz
pure-8140a59205911170e151c95ed5596db7aa0d5ba5.zip
Measure a link on paths of loss, reordering and a bottleneck, against quinn (097)
M6 of design 011: with a 50 Mbit/s bottleneck, a link carries 21-41 times quinn's goodput when packets reorder, the same under random loss, and 1.3-3.5 times with both; its p99 at 50 records a second is the same or shorter; beside TCP CUBIC it takes 0.48-0.55 of the path, inside the guardrail. Only on a clean path is it 3-5% below quinn.
-rw-r--r--docs/design/011-pure-transport.md7
-rw-r--r--docs/research/096-per-delivery-cpu.md2
-rw-r--r--docs/research/097-lossy-paths.md210
-rw-r--r--research/experiments/020-lossy-paths/2026-09-29-paths-linux-aarch64.jsonl126
4 files changed, 344 insertions, 1 deletions
diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md
index d50b593..be25a86 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, M2, M4, M5를 쟀다.
+작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1, M2, M4, M5, M6를 쟀다.
[외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다.
1단계(링크 시제품과 kill -9 검사)는 [093](../research/093-connectionless-link.md)에 있다. 어느 쪽을 죽여도 핸드셰이크 없이
@@ -22,6 +22,11 @@ M5는 [096](../research/096-per-delivery-cpu.md)에 있다. 처음에는 작은
25 % 적고, 61 B에서 1.06–1.12배다. 설계가 적은 송수신 경로(GSO, GRO, 묶어 받기)를 이때 넣었고, 작은 기록을 담는 RECORDS 프레임을
더했다.
+M6는 [097](../research/097-lossy-paths.md)에 있다. 처음에는 패킷 순서만 바뀌어도 링크가 1.5 Mbit/s로 주저앉았다. 늦게 온 패킷을
+알아보고 혼잡 사건을 되돌리게 하고, CUBIC, 패킷 경계에서 자르는 부분, 경로 MTU, 페이싱을 넣었다. 50 Mbit/s 병목에서 재정렬
+아래 quinn의 21–41배, 무작위 손실 아래 같은 처리량, 같거나 짧은 꼬리 지연이다. TCP CUBIC 옆에서 대역의 0.48–0.55를 쓴다.
+가드레일(±25 %) 안이다.
+
2026-09-29에 링크를 `link/`로 뗐다([009](009-layout.md)). 프로토콜은 이제 소켓, 스레드, 시계가 없는 상태이고
(`link/link-protocol.h`), UDP와 스레드는 그 위의 층이다. 그래서 한 스레드가 링크 여럿을 들 수 있다. M2를 이 보내는 쪽으로
다시 돌렸더니 결과가 같았다. 다만 재시작 뒤 가장 늦은 저장이 1초였다. 받았다고 했지만 저장 소식이 없는 기록을 1초 기다리기
diff --git a/docs/research/096-per-delivery-cpu.md b/docs/research/096-per-delivery-cpu.md
index df2718c..fe45954 100644
--- a/docs/research/096-per-delivery-cpu.md
+++ b/docs/research/096-per-delivery-cpu.md
@@ -122,6 +122,8 @@ pure-serve 두 대를 잇고, 보내는 앱에 주문 2만 건을 쌓은 뒤 받
설계해야 한다.
- H4("활성 링크의 전달당 CPU는 QUIC 기준선보다 나쁘지 않다")는 1 KiB 이상에서 선다. 작은 기록에서는 거의 선다. quinn의 값에는
기록마다의 확인과 정확히 한 번이 없다는 점을 함께 본다.
+- 뒤에 M6([097](097-lossy-paths.md))을 하며 세 가지를 바꿨다: 패킷 경계에서 자르는 부분, 경로 MTU(1,452 B), 페이싱이다. 그
+ 뒤 이 조건에서 기록 하나의 CPU가 1 KiB와 16 KiB에서 12 % 더 줄었다. 61 B는 같다(나누기 전 판과 번갈아 세 라운드).
## 한계와 다음
diff --git a/docs/research/097-lossy-paths.md b/docs/research/097-lossy-paths.md
new file mode 100644
index 0000000..c30d9d8
--- /dev/null
+++ b/docs/research/097-lossy-paths.md
@@ -0,0 +1,210 @@
+# 097 — 손실과 재정렬, 병목: 재정렬에 무너지지 않고, 손실에서 QUIC만큼
+
+2026-09-29. [설계 011](../design/011-pure-transport.md)의 M6다. [096](096-per-delivery-cpu.md)이 깨끗한 루프백에서 기록 하나의
+CPU를 쟀다면, 여기서는 지연, 손실, 재정렬, 병목이 있는 경로에서 링크 하나가 얼마나 나르는지, 기록 하나가 얼마나 늦는지, 옆의
+TCP와 대역을 어떻게 나누는지를 quinn과 댄다.
+
+착수 기준:
+
+- **바꿀 결정.** 011의 가드레일이다. TCP CUBIC과 나눈 몫이 ±25 %를 벗어나면(0.375–0.625 밖), 고친 뒤에야 채택한다. 처리량과
+ 꼬리 지연은 quinn과 댄다.
+- **최소 확인.** RTT 20·100 ms, 손실 0·1·5 %, 재정렬, 50 Mbit/s 병목에서 세 가지를 본다. 쌓인 1 KiB 기록을 푸는 처리량, 초당
+ 50건의 꼬리 지연, TCP CUBIC 흐름 옆의 몫이다.
+- **중단 기준.** 위의 ±25 %다.
+
+## 쉬운 말로
+
+- 알아낸 것:
+ - 처음에는 패킷 순서만 바뀌어도 링크가 주저앉았다. 잃은 패킷이 없는데 50 Mbit/s 경로에서 1.5 Mbit/s였다. RFC 9002의 고정
+ 문턱 때문이고, quinn도 같은 규칙을 쓴다.
+ - 늦게 온 패킷을 알아보고 창을 되돌리게 하니, 순서가 흔들려도 흔들리지 않을 때와 같은 45 Mbit/s를 낸다. 같은 경로에서
+ quinn은 1–2 Mbit/s다(21–41배).
+ - 무작위 손실 아래 처리량은 quinn과 같다(0.97–1.07배). 손실과 재정렬이 함께 있으면 quinn의 1.3–3.5배다.
+ - 초당 50건의 꼬리 지연(p99)은 quinn과 같거나 짧다. RTT 100 ms에 손실 5 %에서 418 대 547 ms다.
+ - TCP CUBIC 흐름 옆에서 대역의 0.48–0.55를 쓴다. 가드레일(0.375–0.625) 안이다. 같은 경로에서 quinn은 지연이 1 ms만 흔들려도
+ 0.02–0.07로 밀려난다.
+ - 깨끗한 경로의 처리량만 quinn보다 3–5 % 낮다. 패킷마다 붙는 머리 때문이다.
+- 아직 모르는 것: 실제 무선·셀룰러 경로, 흐름이 많거나 RTT가 서로 다를 때, ngtcp2와의 비교, 저장소 옮기기(M3), 보안 검사.
+- 사용 경험의 차이: 와이파이나 셀룰러처럼 패킷 순서가 흔들리는 망에서도 링크가 느려지지 않는다. 이 에뮬레이션에서 quinn은
+ 1–3 ms의 흔들림만으로 한 자릿수 Mbit/s로 떨어졌다.
+
+## 한 것
+
+### 프로토콜을 시간 밖으로 꺼냈다
+
+측정 전에 링크를 `link/`로 떼고 둘로 나눴다([009](../design/009-layout.md)). `link/link-protocol.h`는 소켓, 스레드, 시계가 없는
+상태다. 패킷과 시각이 들어가고 패킷이 나온다. 그래서 두 쪽을 가상 시간의 네트워크에 놓을 수 있다.
+
+- 시뮬레이터([sim.c](../../research/experiments/020-lossy-paths/sim.c))는 병목(속도, 큐, 드롭테일), 지연, 흔들림(재정렬), 무작위
+ 손실을 흉내 낸다. 같은 씨앗이면 같은 결과가 나온다. 1 KiB 기록 3만 건이 1초 안팎에 끝난다.
+- 처음 돌린 결과가 문제를 곧바로 보여 줬다(RTT 20 ms, 50 Mbit/s, 1 KiB).
+
+| 경로 | 처리량 | 실제로 잃은 패킷 | 손실로 판정 | 가장 작은 창 |
+|---|---:|---:|---:|---:|
+| 깨끗함 | 41 Mbit/s | 0(큐 225) | 225 | 12,000 B |
+| 재정렬만(흔들림 5 ms) | 1.5 Mbit/s | 0 | 308 | 2,400 B |
+| 손실 1 % | 5.5 Mbit/s | 130 | 49 | 4,244 B |
+
+재정렬만 있고 잃은 것이 없는데, 순서가 바뀐 패킷을 손실로 판정해 창이 최소로 주저앉았다. RFC 9002의 고정 문턱(나중 패킷 3개,
+RTT의 9/8) 때문이다. quinn도 같은 규칙을 쓴다.
+
+### 늦게 온 패킷
+
+손실로 판정한 비행을 링에 잠시(RTT 두 번) 남겨 둔다. 그 사이에 확인이 오면 늦게 온 것이다.
+
+- 그 패킷이 나른 부분은 받은 것으로 친다. 아직 다시 보내지 않았으면 다시 보내지 않는다.
+- 문턱을 넓힌다. RACK처럼 두 가지다. 나중 패킷 문턱은 실제로 본 재정렬 깊이로, 시간 문턱은 최소 RTT의 4분의 1씩 늘린다. 재정렬
+ 없는 혼잡 사건이 16번 이어지면 반으로 줄인다.
+- 혼잡 사건을 되돌린다. 앞 사건의 손실이 정해지기 전에 이어진 사건들을 한 에피소드로 묶는다. 에피소드의 손실이 모두 늦게 온
+ 것이면, 첫 사건 전의 창, 문턱, CUBIC 상태로 돌아간다. 하나라도 진짜 손실이면(링에서 늦은 확인 없이 떠나면) 되돌리지 않는다.
+
+시뮬레이터에서 재정렬만 있을 때의 처리량이 RTT 20 ms에서 1.5 → 43.6 Mbit/s, RTT 100 ms에서 10 → 40.1 Mbit/s가 됐다. 재정렬이
+없을 때와 같다.
+
+### CUBIC, 쓰지 않은 창
+
+- 혼잡 회피를 NewReno에서 CUBIC(RFC 9438)으로 바꿨다. quinn의 기본값이고, TCP CUBIC과 나란히 설 때 같은 규칙이어야 한다.
+ Reno 친화 추정은 RTT마다 0.53 데이터그램이다. 배포된 CUBIC(Linux, quinn)이 그렇게 한다. RFC 9438처럼 이전 최대를 넘은 뒤
+ 1로 올리지는 않는다.
+- 창을 다 쓰지 않을 때는(보낼 것이 없거나 받는 쪽 크레딧에 막히면) 창을 키우지 않는다(RFC 9002 §7.8). 전에는 크레딧에 막힌
+ 채로 창이 1만 패킷 넘게 자랐다.
+- Reno 추정은 확인이 올 때만큼, 또는 시간이 가는 만큼(RFC 8312) 가운데 앞선 쪽으로 키운다. 앱이 초당 몇 건만 보내는 링크는
+ 확인이 드물어, 손실 뒤 필요한 창으로 돌아가는 데 오래 걸렸다.
+
+### 기록을 패킷 경계에서 자른다
+
+전에는 기록을 1,000 B 조각으로 나눴다. 1 KiB 기록은 패킷에 하나씩만 들어가 패킷의 13 %가 비었다.
+
+- 이제 패킷의 남은 자리에 64 B 이상 들어가면 기록을 그만큼 자른다. 자른 자리는 바뀌지 않는다. 그래서 한 패킷이 나른 것은 늘
+ 온전한 부분들이다.
+- 받는 쪽은 조각 표 대신 받은 바이트 범위를 합쳐 둔다. 순서대로 오는 부분은 할당 없이 줄 끝에서 바로 이어 붙인다.
+- 루프백에서 패킷이 1 KiB 기록에 8 %, 16 KiB 기록에 10 % 줄었다. 기록 하나의 CPU는 61 B와 1 KiB에서 같고, 16 KiB에서 5 %
+ 줄었다.
+
+### 경로가 나르는 만큼 큰 패킷
+
+1,200 B로 시작한다. RTT를 알면 1,452 B로 채운 PING을 혼자, 조각내지 말라는 표시(DF)를 달고 보낸다(DPLPMTUD, RFC 8899). 확인이
+오면 그 크기를 쓴다. 탐침을 잃는 것은 혼잡이 아니다. 1초 뒤 다시, 세 번까지, 그 뒤로는 10분마다 해 본다.
+
+- CUBIC은 데이터그램 단위로 창을 키운다. 그래서 같은 손실률에서 패킷이 큰 쪽이 바이트로 더 빨리 자란다. quinn과의 손실 아래
+ 차이는 대부분 이것이었다.
+- GSO를 소켓 옵션이 아니라 묶음마다 제어 메시지로 청한다. 소켓 옵션이 걸려 있으면, 혼자 보낸 1,452 B 탐침을 시스템이
+ 1,200 B와 252 B로 잘라 보냈다. `make check`의 UDP 검사가 이것을 잡았다.
+- 루프백에서 패킷이 19–27 % 줄고, 기록 하나의 CPU가 1 KiB에서 12 %, 16 KiB에서 12 % 줄었다([096](096-per-delivery-cpu.md)의
+ 조건, 나누기 전과 번갈아 세 라운드).
+
+### 페이싱
+
+창을 RTT에 걸쳐 1.25배(느린 시작에서 2배)의 빠르기로 보낸다. 한 번에는 2 ms어치(10–256 데이터그램)까지만 보낸다(RFC 9002 §7.7).
+보내는 쪽 상태가 다음에 보낼 수 있는 때를 알려 준다.
+
+### 측정 경로
+
+비특권 이름공간의 루프백 하나를 경로로 쓴다([run.py](../../research/experiments/020-lossy-paths/run.py)).
+
+- 방향마다 netem이 지연(RTT의 절반), 흔들림, 손실을 준다. 받는 쪽 포트(40000–40007)로 가는 쪽만 그 뒤에 tbf 병목(50 Mbit/s, 큐는
+ RTT 한 번어치)을 지난다. 되돌아오는 답과 TCP의 확인은 병목 뒤에 줄 서지 않는다.
+- Pure와 quinn은 GSO 없이 보내고, TCP의 세그먼트는 1,500 B를 넘지 않게 한다. 큐 규칙은 GSO 묶음과 TSO 조각을 패킷 하나로
+ 다루기 때문이다.
+- 몫을 잴 때는 흐름마다 따로 된 프로세스를 동시에 띄우고, 60초 동안 20초부터 센다. 이때 경로의 지연을 1 ms 흔든다. 흔들림이
+ 없으면 드롭테일 큐가 흐름들의 위상만으로 한쪽 패킷을 여덟 배 더 버린다(Floyd와 Jacobson, 1992).
+ - 흔들림 없이 쟀을 때 Pure는 TCP에게서 0.63–0.77, quinn에게서 0.69를 가져갔다.
+ - 같은 조건에서 Pure끼리는 0.47, 큐를 fq_codel로 바꾸면 0.49였다.
+ - 1 ms 흔들림에서는 RTT 20 ms에서 0.53이다.
+- 이 경로를 맞추는 데 측정 여섯 벌을 버렸다. 이유는 이렇다.
+ - tbf 밑에 netem을 두면 tbf의 큐 한도가 사라진다.
+ - 답이 데이터 뒤에 줄 선다.
+ - GSO 묶음이 패킷 하나로 다뤄진다.
+ - 먼저 띄운 흐름이 몫을 더 가져간다.
+ - 흔들림이 없으면 위상 효과가 생긴다.
+ - 하네스가 재정렬을 틀림으로 셌다.
+
+## 결과
+
+라즈베리 파이 5, 비특권 이름공간의 루프백이다. 병목은 50 Mbit/s이고 큐는 RTT 한 번어치다. 번갈아 세 라운드의 중앙값이다.
+Pure는 링크와 리스너만 잰다([transport.c](../../research/experiments/019-idle-links/transport.c), 앱과 저장 없음). quinn은 한
+연결의 한 스트림이다. 기록은 1,013 B(1 KiB 항목의 주문)다. 원시 결과는 `research/experiments/020-lossy-paths/`의
+`2026-09-29-paths-linux-aarch64.jsonl`에 있다. 앞 단계의 결과도 같은 곳에 있다(`-parts-`는 세 라운드, `-mtu-`는 한 라운드).
+
+**쌓인 기록 풀기(Mbit/s, 기록의 바이트).** 흔들림 3 ms면 패킷 순서가 바뀐다.
+
+| RTT | 손실 | 흔들림 | Pure | quinn | Pure/quinn |
+|---:|---:|---:|---:|---:|---:|
+| 20 ms | 0 | 0 | 45.4 | 46.8 | 0.97 |
+| 20 ms | 0 | 3 ms | 45.4 | 2.18 | 20.8 |
+| 20 ms | 1 % | 0 | 6.84 | 6.54 | 1.05 |
+| 20 ms | 1 % | 3 ms | 7.30 | 2.07 | 3.53 |
+| 20 ms | 5 % | 0 | 3.08 | 2.89 | 1.07 |
+| 20 ms | 5 % | 3 ms | 3.27 | 1.62 | 2.02 |
+| 100 ms | 0 | 0 | 44.9 | 47.3 | 0.95 |
+| 100 ms | 0 | 3 ms | 45.2 | 1.11 | 40.8 |
+| 100 ms | 1 % | 0 | 1.58 | 1.56 | 1.01 |
+| 100 ms | 1 % | 3 ms | 1.43 | 0.82 | 1.74 |
+| 100 ms | 5 % | 0 | 0.60 | 0.62 | 0.97 |
+| 100 ms | 5 % | 3 ms | 0.65 | 0.49 | 1.33 |
+
+**단계마다, RTT 20 ms에서.** 무엇이 얼마나 바꿨는지다.
+
+| Pure | 깨끗함 | 재정렬 | 손실 1 % | 손실 5 % |
+|---|---:|---:|---:|---:|
+| 늦은 패킷, CUBIC, 부분까지(세 라운드) | 44.3 | 44.3 | 5.83 | 2.26 |
+| + 경로 MTU(한 라운드) | 45.4 | 45.4 | 8.24 | 2.80 |
+| + 페이싱, Reno 0.53(세 라운드, 최종) | 45.4 | 45.4 | 6.84 | 3.08 |
+| quinn(최종과 같은 때) | 46.8 | 2.18 | 6.54 | 2.89 |
+
+경로 MTU를 쓴 판의 손실 1 % 값(8.24)은 RFC 9438처럼 Reno 추정을 이전 최대 뒤에 RTT마다 1 데이터그램으로 키운 판이다. 배포된
+CUBIC과 같게 0.53으로 두니 6.84가 됐다.
+
+**초당 50건의 꼬리 지연(ms, 넣은 때부터 넘겨받은 때까지).** Pure / quinn이다.
+
+| RTT | 손실 | p50 | p99 | 최대 |
+|---:|---:|---|---|---|
+| 20 ms | 0 | 10 / 10 | 10 / 10 | 10 / 10 |
+| 20 ms | 1 % | 10 / 10 | 50 / 50 | 50 / 83 |
+| 20 ms | 5 % | 10 / 10 | 70 / 96 | 90 / 137 |
+| 100 ms | 0 | 50 / 50 | 50 / 50 | 50 / 50 |
+| 100 ms | 1 % | 50 / 50 | 170 / 196 | 190 / 282 |
+| 100 ms | 5 % | 70 / 110 | 418 / 547 | 517 / 686 |
+
+잃은 패킷은 다음 패킷의 확인이 오는 순간에 손실로 판정해 곧바로 다시 보낸다(RTT 100 ms, 20 ms 간격이면 보낸 뒤 약 120 ms).
+p99가 170 ms인 까닭이다. 시뮬레이터도 같은 값을 준다.
+
+**TCP CUBIC 옆의 몫(60초 가운데 20초부터, 경로 지연 1 ms 흔들림).**
+
+| RTT | Pure | 라운드별 | TCP끼리 | quinn |
+|---:|---:|---|---:|---:|
+| 20 ms | 0.484 | 0.517, 0.484, 0.483 | 0.518 | 0.065 |
+| 100 ms | 0.549 | 0.560, 0.545, 0.549 | 0.482 | 0.018 |
+
+- TCP끼리의 몫도 라운드마다 0.44–0.58 사이에서 움직인다. Pure는 그 범위 안이다.
+- quinn은 1 ms 흔들림을 손실로 읽어 창을 줄인다. 같은 흔들림에서 quinn 혼자 몰아 보낼 때도 1–2 Mbit/s였다.
+- 큐를 fq_codel로 바꾸면(흐름마다 공정하게 줄 세움, 한 라운드) Pure 0.49·0.48, quinn 0.50·0.50이다.
+
+## 판정
+
+- **가드레일은 통과다.** TCP CUBIC 옆에서 Pure의 몫은 0.48(RTT 20 ms)과 0.55(RTT 100 ms)다. 라운드별로 0.48–0.56으로,
+ 0.375–0.625 안이고 TCP끼리의 흔들림과 같은 범위다.
+- **M6의 나머지.**
+ - 재정렬에서 quinn의 21–41배다.
+ - 무작위 손실에서는 같다(0.97–1.07배).
+ - 손실과 재정렬이 함께 있으면 1.3–3.5배다.
+ - 꼬리 지연은 같거나 짧다.
+ - 깨끗한 경로에서만 3–5 % 낮다.
+- **M5는 더 좋아졌다.** 부분, 경로 MTU, 페이싱을 넣은 뒤 기록 하나의 CPU가 1 KiB와 16 KiB에서 12 % 줄었다. 61 B는 같다
+ (096의 조건).
+- 011의 5단계("M5·M6 가드레일을 잰다")가 끝났다. 남은 것은 4단계다: 에폭 바꿈, 경로 바뀜, 저장소 옮기기(H3, M3), 보안 검사.
+
+## 한계와 다음
+
+- **경로는 에뮬레이션이다.**
+ - 루프백 하나에 netem과 tbf를 얹었다. 실제 무선·셀룰러, ECMP의 재정렬, 여러 AQM이 아니다.
+ - 몫은 두 흐름, 같은 RTT만 봤다.
+- **무작위 손실 아래는 손실을 혼잡으로 읽는 모든 제어와 같다.** 손실 5 %, RTT 100 ms에서 0.6 Mbit/s다. 넘으려면 모델 기반 제어
+ (BBR 계열)나 여분 전송이 필요하다. 그것은 가드레일(TCP와의 몫)과 함께 따져야 한다.
+- **꼬리 지연은 확인으로 알 수 있는 가장 이른 때에 다시 보낸다.** 더 줄이려면, 앱이 한가해 창이 빌 때 기록을 한 번 더 보내는
+ 여분이 필요하다.
+- **깨끗한 경로의 3–5 %.** 패킷마다 스트림 이름(약 25 B)을 싣는다. 096의 짧은 이름표 과제와 같다.
+- **경로 MTU는 늘리기만 한다.** 경로가 도중에 좁아지면(블랙홀) 알아채지 못한다. RFC 8899의 블랙홀 탐지는 다음이다.
+- **페이싱은 보내는 스레드의 poll(ms 단위)에 묶인다.**
+- **QUIC 비교는 quinn 0.11 하나다.** GSO 없이 쟀다. ngtcp2는 아직이다. quinn이 흔들림에 무너진 것은 RFC 9002의 고정 문턱 때문으로
+ 보지만, quinn의 코드로 확인하지는 않았다.
diff --git a/research/experiments/020-lossy-paths/2026-09-29-paths-linux-aarch64.jsonl b/research/experiments/020-lossy-paths/2026-09-29-paths-linux-aarch64.jsonl
new file mode 100644
index 0000000..fd25e28
--- /dev/null
+++ b/research/experiments/020-lossy-paths/2026-09-29-paths-linux-aarch64.jsonl
@@ -0,0 +1,126 @@
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 45.39, "resent": 502, "lost": 251, "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": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 45.39, "resent": 523, "lost": 262, "late": 22, "undone": 8, "packets": 84090}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 2.15, "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.97, "resent": 254, "lost": 127, "late": 0, "undone": 0, "packets": 13289}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 6.34, "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.3, "resent": 369, "lost": 192, "late": 39, "undone": 19, "packets": 14528}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 2.07, "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": 3.08, "resent": 616, "lost": 290, "late": 0, "undone": 0, "packets": 5987}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 2.89, "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.27, "resent": 687, "lost": 357, "late": 40, "undone": 24, "packets": 6517}
+{"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": "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.118, "waited_ms_max": 10.319, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.055, "waited_ms_p99": 10.182, "waited_ms_max": 10.272}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.036, "waited_ms_p99": 50.032, "waited_ms_max": 51.068, "resent": 12, "lost": 12, "late": 0, "undone": 0, "packets": 1013}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.07, "waited_ms_p99": 56.362, "waited_ms_max": 96.848}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 10.045, "waited_ms_p99": 50.152, "waited_ms_max": 90.101, "resent": 51, "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.068, "waited_ms_p99": 96.603, "waited_ms_max": 190.435}
+{"kind": "share", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 24.05, "tcp_mbit": 22.47, "share": 0.517}
+{"kind": "share", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 3.1, "tcp_mbit": 44.63, "share": 0.065}
+{"kind": "share", "who": "tcp", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 24.75, "tcp_mbit": 23.07, "share": 0.518}
+{"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": 107900}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 47.27, "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": 34, "lost": 19, "late": 19, "undone": 10, "packets": 86008}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 1.11, "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.58, "resent": 46, "lost": 23, "late": 0, "undone": 0, "packets": 2933}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 1.56, "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.43, "resent": 78, "lost": 40, "late": 8, "undone": 2, "packets": 2934}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 0.68, "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.67, "resent": 121, "lost": 59, "late": 0, "undone": 0, "packets": 1421}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.57, "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.69, "resent": 131, "lost": 67, "late": 10, "undone": 3, "packets": 1304}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 0, "mbit": 0.44, "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.045, "waited_ms_p99": 50.099, "waited_ms_max": 50.411, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.051, "waited_ms_p99": 50.171, "waited_ms_max": 50.228}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.038, "waited_ms_p99": 169.988, "waited_ms_max": 190.054, "resent": 9, "lost": 9, "late": 0, "undone": 0, "packets": 1007}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 50.066, "waited_ms_p99": 190.765, "waited_ms_max": 282.411}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 70.15, "waited_ms_p99": 417.557, "waited_ms_max": 517.231, "resent": 61, "lost": 49, "late": 0, "undone": 0, "packets": 959}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 0, "mbit": 0.41, "waited_ms_p50": 96.372, "waited_ms_p99": 546.84, "waited_ms_max": 685.711}
+{"kind": "share", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 26.0, "tcp_mbit": 20.43, "share": 0.56}
+{"kind": "share", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 0.77, "tcp_mbit": 47.03, "share": 0.016}
+{"kind": "share", "who": "tcp", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 0, "mbit": 20.92, "tcp_mbit": 26.9, "share": 0.437}
+{"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": 84109}
+{"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": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 45.38, "resent": 538, "lost": 271, "late": 25, "undone": 8, "packets": 84939}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 2.18, "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.38, "resent": 266, "lost": 133, "late": 0, "undone": 0, "packets": 12304}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 6.54, "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.06, "resent": 365, "lost": 190, "late": 36, "undone": 18, "packets": 14345}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 2.03, "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": 3.11, "resent": 598, "lost": 274, "late": 0, "undone": 0, "packets": 6164}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 3.01, "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.27, "resent": 653, "lost": 337, "late": 40, "undone": 22, "packets": 6329}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.57, "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.036, "waited_ms_p99": 10.116, "waited_ms_max": 10.428, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.057, "waited_ms_p99": 10.187, "waited_ms_max": 10.259}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.043, "waited_ms_p99": 30.126, "waited_ms_max": 50.143, "resent": 9, "lost": 9, "late": 0, "undone": 0, "packets": 1010}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.066, "waited_ms_p99": 50.37, "waited_ms_max": 83.454}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.046, "waited_ms_p99": 70.108, "waited_ms_max": 90.145, "resent": 64, "lost": 64, "late": 0, "undone": 0, "packets": 1066}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 10.067, "waited_ms_p99": 84.667, "waited_ms_max": 124.965}
+{"kind": "share", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 22.58, "tcp_mbit": 24.03, "share": 0.484}
+{"kind": "share", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 3.23, "tcp_mbit": 44.5, "share": 0.068}
+{"kind": "share", "who": "tcp", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 25.86, "tcp_mbit": 21.96, "share": 0.541}
+{"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": 107727}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 47.27, "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": 50, "lost": 26, "late": 26, "undone": 13, "packets": 85881}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 1.11, "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.49, "resent": 66, "lost": 33, "late": 0, "undone": 0, "packets": 3884}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 1.3, "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.09, "resent": 80, "lost": 39, "late": 4, "undone": 2, "packets": 2579}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.98, "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.6, "resent": 150, "lost": 66, "late": 0, "undone": 0, "packets": 1290}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "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": 1, "mbit": 0.65, "resent": 134, "lost": 68, "late": 8, "undone": 4, "packets": 1282}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 1, "mbit": 0.49, "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.036, "waited_ms_p99": 50.129, "waited_ms_max": 50.336, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.066, "waited_ms_p99": 50.178, "waited_ms_max": 50.818}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.042, "waited_ms_p99": 170.136, "waited_ms_max": 250.16, "resent": 12, "lost": 11, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.074, "waited_ms_p99": 196.403, "waited_ms_max": 236.965}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 50.243, "waited_ms_p99": 330.278, "waited_ms_max": 437.53, "resent": 57, "lost": 43, "late": 0, "undone": 0, "packets": 968}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 1, "mbit": 0.41, "waited_ms_p50": 130.682, "waited_ms_p99": 751.983, "waited_ms_max": 872.947}
+{"kind": "share", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 25.2, "tcp_mbit": 21.08, "share": 0.545}
+{"kind": "share", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 1.07, "tcp_mbit": 46.74, "share": 0.022}
+{"kind": "share", "who": "tcp", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 1, "mbit": 27.54, "tcp_mbit": 20.27, "share": 0.576}
+{"kind": "bulk", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 45.39, "resent": 498, "lost": 249, "late": 0, "undone": 0, "packets": 84110}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 46.77, "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": 2, "mbit": 45.38, "resent": 499, "lost": 249, "late": 21, "undone": 5, "packets": 84042}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 2.22, "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": 2, "mbit": 6.84, "resent": 272, "lost": 136, "late": 0, "undone": 0, "packets": 13293}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 6.64, "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": 2, "mbit": 7.92, "resent": 318, "lost": 166, "late": 32, "undone": 20, "packets": 14994}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 2.13, "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": 2, "mbit": 3.04, "resent": 641, "lost": 301, "late": 0, "undone": 0, "packets": 5951}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 2.87, "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": 2, "mbit": 3.24, "resent": 657, "lost": 339, "late": 38, "undone": 17, "packets": 6303}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 1.62, "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": 2, "mbit": 0.41, "waited_ms_p50": 10.037, "waited_ms_p99": 10.119, "waited_ms_max": 10.424, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 10.058, "waited_ms_p99": 10.178, "waited_ms_max": 10.268}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 10.048, "waited_ms_p99": 50.028, "waited_ms_max": 50.188, "resent": 10, "lost": 10, "late": 0, "undone": 0, "packets": 1011}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 10.068, "waited_ms_p99": 50.282, "waited_ms_max": 82.48}
+{"kind": "paced", "who": "pure", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 10.038, "waited_ms_p99": 70.127, "waited_ms_max": 93.141, "resent": 56, "lost": 54, "late": 0, "undone": 0, "packets": 1055}
+{"kind": "paced", "who": "quinn", "rtt_ms": 20, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 10.065, "waited_ms_p99": 95.533, "waited_ms_max": 136.894}
+{"kind": "share", "who": "pure", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 22.53, "tcp_mbit": 24.08, "share": 0.483}
+{"kind": "share", "who": "quinn", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 2.65, "tcp_mbit": 45.07, "share": 0.056}
+{"kind": "share", "who": "tcp", "rtt_ms": 20, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 24.33, "tcp_mbit": 23.49, "share": 0.509}
+{"kind": "bulk", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 44.86, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 108043}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 47.27, "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": 2, "mbit": 45.24, "resent": 31, "lost": 17, "late": 17, "undone": 10, "packets": 86095}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 1.17, "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": 2, "mbit": 1.6, "resent": 70, "lost": 35, "late": 0, "undone": 0, "packets": 3670}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 1.71, "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": 2, "mbit": 1.45, "resent": 86, "lost": 43, "late": 6, "undone": 3, "packets": 3251}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 0.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": 2, "mbit": 0.56, "resent": 156, "lost": 69, "late": 0, "undone": 0, "packets": 1272}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.62, "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": 2, "mbit": 0.63, "resent": 179, "lost": 89, "late": 9, "undone": 3, "packets": 1442}
+{"kind": "bulk", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 3, "seconds": 20, "round": 2, "mbit": 0.5, "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": 2, "mbit": 0.41, "waited_ms_p50": 50.039, "waited_ms_p99": 50.113, "waited_ms_max": 50.414, "resent": 0, "lost": 0, "late": 0, "undone": 0, "packets": 1001}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 50.054, "waited_ms_p99": 50.178, "waited_ms_max": 50.26}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 50.035, "waited_ms_p99": 170.071, "waited_ms_max": 190.107, "resent": 9, "lost": 9, "late": 0, "undone": 0, "packets": 1009}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 1, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 50.088, "waited_ms_p99": 389.311, "waited_ms_max": 508.039}
+{"kind": "paced", "who": "pure", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.4, "waited_ms_p50": 90.061, "waited_ms_p99": 610.258, "waited_ms_max": 690.166, "resent": 77, "lost": 48, "late": 0, "undone": 0, "packets": 946}
+{"kind": "paced", "who": "quinn", "rtt_ms": 100, "loss": 5, "jitter_ms": 0, "seconds": 20, "round": 2, "mbit": 0.41, "waited_ms_p50": 110.498, "waited_ms_p99": 506.435, "waited_ms_max": 586.416}
+{"kind": "share", "who": "pure", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 25.52, "tcp_mbit": 20.93, "share": 0.549}
+{"kind": "share", "who": "quinn", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 0.88, "tcp_mbit": 46.92, "share": 0.018}
+{"kind": "share", "who": "tcp", "rtt_ms": 100, "loss": 0, "jitter_ms": 1, "seconds": 60, "round": 2, "mbit": 23.04, "tcp_mbit": 24.78, "share": 0.482}