aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 19:45:25 +0100
committereunhhu <[email protected]>2026-09-27 19:45:25 +0100
commitbac70f376ef539a03a25e187f706fe8f9f67f01e (patch)
treedb8339687c2858477e2d23bb61509b85c3dafd99
parentd72f57af85bb57042332da8bc002a2a0640639a0 (diff)
downloadpure-bac70f376ef539a03a25e187f706fe8f9f67f01e.tar.gz
pure-bac70f376ef539a03a25e187f706fe8f9f67f01e.zip
Record the messenger between two nodes and its latency (089)
The roadmap's E2EE messenger made of the runtime's general parts, with no messenger code in it: sealed, authenticated sessions between nodes (086, 088), Vega on each node's display as a surface (design 010), the app in Vega's examples. Vega's check runs two nodes through a recording relay: a line typed on one display is drawn on the other, frames match the vega command's, and the relay sees no text. A line takes 0.79 ms at p50 and 1.1 ms at p99 from one node's program to the other node's views with stores in RAM, both commits, the HTTP delivery and the seal included (sealing adds nothing measurable); on the Pi's SD card 3.5 ms at p50, the long tail being the card's syncs.
-rw-r--r--CHECKPOINT.md3
-rw-r--r--README.md8
-rw-r--r--docs/research/089-messenger.md80
-rw-r--r--research/experiments/015-messenger-latency/2026-09-27-sd-and-ram-linux-aarch64.jsonl8
-rw-r--r--research/experiments/015-messenger-latency/latency.py120
5 files changed, 215 insertions, 4 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 3373ba7..d144896 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -15,8 +15,9 @@
| 앱 표현 | 상태(수, text와 커서·마크, 레코드의 리스트와 키 있는 리스트, 인덱스), 입력(키, text, 필드 있는 메시지, 타이머), 원자적 이벤트 핸들러, 순수 보기(조건식, 표와 걸러진 표, count·sum·max·min·first), `fn`·`use` 라이브러리, text 비교, 시간(`now`, `every`, `floor`, `%`). 로드할 때 타입·스택·합류·커서와 인덱스의 소속을 모두 검증하고 이벤트의 실행량에 상한이 있다 | 008, aup `docs/design/002-dsl-direction.md`, aup `make check`(파이썬 참조 모델 대조, 진단 132건) |
| 영속 | 선언한 상태가 곧 영속 상태다. 트리 저장소에 커밋당 동기화 한 번(헤드를 바이트 바로 앞에 두어 한 연속 구간), 이벤트 묶음마다 저장 한 번. `kill -9` 뒤 확인 응답한 이벤트가 모두 남는다. 레코드·키가 바뀐 모듈로 바꿔도 데이터가 이름으로 옮겨진다 | [076](docs/research/076-one-sync-commit.md), [081](docs/research/081-tail-records.md), 008 상태 영속·교체 |
| 인터페이스 | 한 명세가 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, 봉인 없이 466k/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) |
+| 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) |
+| 표면과 메신저 | 사람을 위한 표면이 노드 안에서 돈다(`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) |
키로 나눈 소유자와 저장을 넘길 스레드는 하지 않기로 했다. 이 장치는 동기화를 한 줄로 세워, 나누면 쓰기가 23% 줄고 넘겨도
기기의 일은 줄지 않는다([084](docs/research/084-parallel-syncs.md)). 대신 압축한 파일이 한 주기의 0을 미리 가져 커밋이 새
diff --git a/README.md b/README.md
index bf9c167..b9fa6e4 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,14 @@ terminal UI, a CLI, spoken sentences and a web page through [Vega](/srv/git/vega
durable, a commit being one contiguous write and one sync; views are read from a snapshot of the last save, so
reads never wait for a sync. What an app sends is saved with its event and delivered over HTTP in order and in
batches, and a receiving app applies each message once; sealed to the receiver's key (HPKE, one session a wire,
-authenticated by the sender's key), no relay between them can read or change it, at the speed of unsealed sends. Measurements against axum and drogon on a Raspberry Pi 5
+authenticated by the sender's key), no relay between them can read or change it, at the speed of unsealed sends.
+People use a node through surfaces it runs, reading its states where they are (Vega's display: a two-node messenger
+shows a line on the other node's screen in about a millisecond). Measurements against axum and drogon on a Raspberry Pi 5
are in [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),
[082](docs/research/082-json-in-place.md), [083](docs/research/083-delivery-over-http.md),
-[086](docs/research/086-sealed-sends.md), [087](docs/research/087-small-reads-and-idle-memory.md) and
-[088](docs/research/088-sealed-sessions.md); the status table is
+[086](docs/research/086-sealed-sends.md), [087](docs/research/087-small-reads-and-idle-memory.md),
+[088](docs/research/088-sealed-sessions.md) and [089](docs/research/089-messenger.md); the status table is
[CHECKPOINT.md](CHECKPOINT.md).
The source follows the principle below, a small core with the rest selected around it
diff --git a/docs/research/089-messenger.md b/docs/research/089-messenger.md
new file mode 100644
index 0000000..a74107d
--- /dev/null
+++ b/docs/research/089-messenger.md
@@ -0,0 +1,80 @@
+# 089 — 메신저: 두 노드, 끝에서 끝까지 봉인, 노드 안의 화면
+
+2026-09-27. 로드맵의 "E2EE 초저지연 메신저"를 런타임 쪽 능력으로 처음 끝까지 이은 것. 앱은 Vega의
+`examples/messenger.aup`이다(앱 기능은 런타임 밖에 둔다). 여기서는 런타임이 무엇을 주고 얼마나 걸리는지를 적는다.
+
+착수 기준:
+
+- 바꿀 결정: 메신저를 이루는 것(봉인한 전달, 보낸 이 인증, 사람이 쓰는 화면, 그 사이의 지연)을 런타임의 일반 능력으로 줄지,
+ 메신저 전용 코드로 만들지. 전용 코드는 쓰지 않는다. 086·088의 봉인 전달과 설계 010의 표면으로 된다는 것을 보인다.
+- 최소 확인: 두 노드가 서로에게 봉인해 보내고, 한쪽 화면에서 친 줄이 다른 쪽 화면에 그려지며, 사이의 중계는 평문을 보지
+ 못한다는 것을 검사로 지킨다. 한 줄이 상대 노드에 보이기까지의 시간을 봉인 유무와 저장 장치별로 잰다.
+- 중단 기준: 메신저를 위해 런타임에 앱 전용 개념(대화, 연락처)을 넣어야 한다면 멈추고 다시 설계한다.
+
+## 이어진 것
+
+| 로드맵이 말한 것 | 무엇으로 |
+|---|---|
+| 끝에서 끝까지 암호(E2EE) | HPKE 봉인(RFC 9180). 전달선마다 세션 하나, 보내는 노드의 키로 auth 모드([088](088-sealed-sessions.md)) |
+| P2P | 노드에서 노드로. 중계·프록시가 사이에 있어도 읽지도 바꾸지도 못한다([086](086-sealed-sends.md)) |
+| 하드웨어 암호 직결 | CPU에 AES 명령이 있으면 AES-128-GCM(이 A76에서 ChaCha20-Poly1305의 2–3배), 없으면 ChaCha20-Poly1305 |
+| 메모리 포렌식 면역 | 연 평문은 요청 티켓이 반납될 때 0으로 지운다. 개인키는 OpenSSL 보안 힙에 둔다(잠김, 코어 덤프 제외, 해제 때 지움) |
+| 화면 | 노드 안의 Vega([설계 010](../design/010-surfaces.md)). 마지막 저장을 제자리에서 읽고, 친 키와 글자는 소유자의 줄로 |
+
+앱은 이렇게 생겼다. 줄 목록 상태, 화면에서 치는 텍스트 입력 `say`, 상대 노드가 보내는 메시지 입력 `heard`, 상대에게 가는
+출력 `said`다. `on say`가 줄을 더하고 `send said`한다. 노드는 이렇게 연결한다.
+
+```sh
+pure-serve --module messenger.pmod --store alice --key alice.key --name alice \
+ --send said=http://bob:8802/inputs/heard --seal said=BOB_KEY --sender bob.said=BOB_KEY \
+ --surface vega-surface.so -- display --symbols ui.vsym --live
+```
+
+런타임에 들어간 메신저 전용 코드는 없다. 대화와 연락처는 앱의 상태이고, 보낸 이는 전달의 출처와 키이며, 화면은 표면이다.
+
+## 검사
+
+Vega의 `tests/check.py`의 `messenger`는 앨리스와 밥의 노드를 띄운다. 각자 제 키를 갖고, 서로에게 봉인하며, 상대만을 보낸
+이로 받는다. 둘 사이에는 기록하는 중계를 둔다. 각 노드에는 Vega가 픽셀 표면으로 들어 있어, 저장마다 한 프레임을 파일로
+그린다.
+
+1. 앨리스의 화면에 녹화한 키로 "hello bob"과 Enter를 친다.
+2. 밥의 노드가 그 줄을 보인다.
+3. 밥은 프로그램으로(HTTP) "hi alice"를 보낸다.
+4. 앨리스의 노드와 화면에 그 줄이 그려진다.
+
+검사는 다음을 확인한다.
+
+- 두 노드의 마지막 프레임이 각 노드의 저장소를 `vega pixels`로 다시 그린 것과 바이트까지 같다.
+- 중계가 본 본문에는 줄의 글자도 필드 이름도 없다.
+- 방향마다 봉인 세션 하나가 있고, 봉인은 보낸 이의 키를 적는다.
+
+Vega 검사 25개 모두와 함께 통과한다.
+
+## 지연
+
+한 프로그램이 앨리스의 노드에 줄 300개를 하나씩 보내고 둘을 잰다. 하나는 앨리스의 노드가 저장해 답할 때까지이고, 다른
+하나는 밥의 노드가 그 줄을 보일 때까지다(밥의 제목 보기를 0.2 ms마다 읽는다). 두 노드는 직접 연결했다. 서버는 코어 0–2를
+쓰고, 변형의 순서는 라운드마다 돌렸다([latency.py](../../research/experiments/015-messenger-latency/latency.py),
+[원시 결과](../../research/experiments/015-messenger-latency/2026-09-27-sd-and-ram-linux-aarch64.jsonl)).
+
+| 저장소 | 봉인 | 앨리스 저장 p50 | 밥에게 보임 p50 | p90 | p99 |
+|---|---|---:|---:|---:|---:|
+| RAM(`/dev/shm`) | 봉인·인증 | 0.34 ms | **0.79 ms** | 0.82–0.83 ms | **1.09–1.13 ms** |
+| RAM | 없음 | 0.33 ms | 0.78 ms | 0.81–0.84 ms | 0.89–1.05 ms |
+| SD 카드(ext4) | 봉인·인증 | 1.64–1.72 ms | 3.45–3.69 ms | 6.7–6.8 ms | 62–78 ms |
+| SD 카드 | 없음 | 1.65–1.84 ms | 3.42–3.72 ms | 6.6–10.0 ms | 62–84 ms |
+
+한 줄이 두 노드의 내구 커밋 두 번, HTTP 전달 하나, 봉인과 열기를 거쳐 상대에게 보이기까지 1 ms 안쪽이다. 봉인의 몫은 잡음
+안이다. SD 카드의 긴 꼬리는 동기화 자체다. 앨리스의 저장 p99가 이미 20–66 ms이고, 밥의 저장이 또 하나를 더한다. 소프트웨어가
+줄일 몫이 아니다. NVMe 같은 장치에서는 RAM 쪽에 가까울 것이다(잴 장치가 아직 없다).
+
+## 한계와 다음
+
+- **전방 비밀성**: 없다. 받는 노드의 키를 훔치면 기록해 둔 세션이 열린다. 받는 키를 돌려 쓰기(서명한 사전 키)가 다음이다.
+- **보낸 이 인증**: auth 모드는 키 누설 사칭(KCI)에 약하다. 받는 노드의 키를 가진 쪽은 그 노드에게 누구로든 보낼 수 있다(088).
+- **오프라인**: 받는 노드가 꺼져 있으면 보내는 노드의 보낼 함(저장된 send)이 기다린다. 두 노드가 동시에 켜져 있어야 간다.
+ 늘 켜진 우편함 노드(봉인한 요청을 열지 않고 맡아 두었다 전하는 것)가 다음 후보다.
+- **여럿**: 한 출력은 한 수신자에게 간다. 연락처가 여럿이면 필드로 수신자를 고르는 전달이 필요하다.
+- **메타데이터**: 출처, 번호, 크기, 보낸 이의 공개키가 중계에 보인다.
+- **화면**: 노드 안의 표면은 키와 글자만 준다. 가리키기(터치로 행 고르기)는 표면 표에 아직 없다.
diff --git a/research/experiments/015-messenger-latency/2026-09-27-sd-and-ram-linux-aarch64.jsonl b/research/experiments/015-messenger-latency/2026-09-27-sd-and-ram-linux-aarch64.jsonl
new file mode 100644
index 0000000..41633eb
--- /dev/null
+++ b/research/experiments/015-messenger-latency/2026-09-27-sd-and-ram-linux-aarch64.jsonl
@@ -0,0 +1,8 @@
+{"round": 0, "directory": "/var/tmp", "sealed": true, "lines": 300, "saved_p50_ms": 1.64, "saved_p99_ms": 66.34, "delivered_p50_ms": 1.82, "delivered_p99_ms": 22.69, "shown_p50_ms": 3.45, "shown_p90_ms": 6.81, "shown_p99_ms": 77.82, "shown_max_ms": 82.88}
+{"round": 0, "directory": "/var/tmp", "sealed": false, "lines": 300, "saved_p50_ms": 1.84, "saved_p99_ms": 61.26, "delivered_p50_ms": 1.83, "delivered_p99_ms": 66.79, "shown_p50_ms": 3.72, "shown_p90_ms": 9.96, "shown_p99_ms": 83.57, "shown_max_ms": 285.19}
+{"round": 1, "directory": "/var/tmp", "sealed": false, "lines": 300, "saved_p50_ms": 1.65, "saved_p99_ms": 20.78, "delivered_p50_ms": 1.68, "delivered_p99_ms": 59.8, "shown_p50_ms": 3.42, "shown_p90_ms": 6.58, "shown_p99_ms": 62.0, "shown_max_ms": 83.38}
+{"round": 1, "directory": "/var/tmp", "sealed": true, "lines": 300, "saved_p50_ms": 1.72, "saved_p99_ms": 59.58, "delivered_p50_ms": 1.76, "delivered_p99_ms": 55.84, "shown_p50_ms": 3.69, "shown_p90_ms": 6.71, "shown_p99_ms": 62.16, "shown_max_ms": 80.68}
+{"round": 0, "directory": "/dev/shm", "sealed": true, "lines": 300, "saved_p50_ms": 0.34, "saved_p99_ms": 0.58, "delivered_p50_ms": 0.45, "delivered_p99_ms": 0.63, "shown_p50_ms": 0.79, "shown_p90_ms": 0.83, "shown_p99_ms": 1.13, "shown_max_ms": 4.32}
+{"round": 0, "directory": "/dev/shm", "sealed": false, "lines": 300, "saved_p50_ms": 0.33, "saved_p99_ms": 0.4, "delivered_p50_ms": 0.45, "delivered_p99_ms": 0.51, "shown_p50_ms": 0.78, "shown_p90_ms": 0.81, "shown_p99_ms": 0.89, "shown_max_ms": 0.9}
+{"round": 1, "directory": "/dev/shm", "sealed": false, "lines": 300, "saved_p50_ms": 0.33, "saved_p99_ms": 0.48, "delivered_p50_ms": 0.45, "delivered_p99_ms": 0.56, "shown_p50_ms": 0.78, "shown_p90_ms": 0.84, "shown_p99_ms": 1.05, "shown_max_ms": 3.28}
+{"round": 1, "directory": "/dev/shm", "sealed": true, "lines": 300, "saved_p50_ms": 0.34, "saved_p99_ms": 0.52, "delivered_p50_ms": 0.45, "delivered_p99_ms": 0.61, "shown_p50_ms": 0.79, "shown_p90_ms": 0.82, "shown_p99_ms": 1.09, "shown_max_ms": 3.62}
diff --git a/research/experiments/015-messenger-latency/latency.py b/research/experiments/015-messenger-latency/latency.py
new file mode 100644
index 0000000..5c67f88
--- /dev/null
+++ b/research/experiments/015-messenger-latency/latency.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+"""How long a line takes from one messenger node to the other (docs/research/089-messenger.md).
+
+Two nodes of Vega's examples/messenger.aup on this machine, Alice's and Bob's,
+wired to each other directly: sealed (each with a key of its own, sealing to
+the other's, naming the other as its one sender) or plain. A program posts
+--lines lines to Alice's node one after another; for each it times the post
+(answered once Alice's node saved the line) and the moment Bob's node shows
+it (its title view counts the line; polled every 0.2 ms). Nodes on cores 0-2,
+stores on --directory. Prints one JSON line per variant and round.
+"""
+import argparse
+import http.client
+import json
+import os
+import select
+import shutil
+import signal
+import statistics
+import subprocess
+import tempfile
+import time
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parents[2]
+
+
+def start(serve, module, work, name, *extra):
+ process = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--module', str(module), '--store',
+ str(work / f'{name}-store'), '--port', '0', '--name', name, *map(str, extra)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ ready, _, _ = select.select([process.stdout], [], [], 10)
+ assert ready, 'node did not start'
+ return process, json.loads(process.stdout.readline())
+
+
+def stop(process):
+ process.send_signal(signal.SIGTERM)
+ _, error = process.communicate(timeout=30)
+ assert process.returncode == 0, error[-500:]
+
+
+class Client:
+ def __init__(self, port):
+ self.connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+
+ def call(self, method, path, body=None):
+ self.connection.request(method, path, body)
+ answer = self.connection.getresponse()
+ return answer.status, answer.read().decode()
+
+
+def run(serve, module, work, sealed, lines):
+ for name in ('alice', 'bob'):
+ shutil.rmtree(work / f'{name}-store', ignore_errors=True)
+ keys = {}
+ for name in ('alice', 'bob'):
+ extra = ['--key', work / f'{name}.key'] if sealed else []
+ process, started = start(serve, module, work, name, *extra)
+ keys[name] = started.get('public_key')
+ assert Client(started['port']).call('POST', '/inputs/rename', name.encode())[0] == 200
+ stop(process)
+ # Bob first, on a port of its own; then Alice, wired to it; then Bob again, wired to Alice.
+ def wiring(name, other, port):
+ wire = ['--send', f'said=http://127.0.0.1:{port}/inputs/heard']
+ if sealed:
+ wire += ['--key', work / f'{name}.key', '--seal', f'said={keys[other]}', '--sender',
+ f'{other}.said={keys[other]}']
+ return wire
+ bob, bob_started = start(serve, module, work, 'bob', *(['--key', work / 'bob.key'] if sealed else []))
+ alice, alice_started = start(serve, module, work, 'alice', *wiring('alice', 'bob', bob_started['port']))
+ to_alice, to_bob = Client(alice_started['port']), Client(bob_started['port'])
+ saved, seen = [], []
+ try:
+ for i in range(lines):
+ began = time.perf_counter()
+ assert to_alice.call('POST', '/inputs/say', f'line {i}'.encode())[0] == 200
+ saved.append(time.perf_counter() - began)
+ want = f'bob: {i + 1} lines'
+ while to_bob.call('GET', '/views/title')[1] != want:
+ time.sleep(0.0002)
+ seen.append(time.perf_counter() - began)
+ finally:
+ stop(alice)
+ stop(bob)
+
+ def ms(values, q):
+ return round(statistics.quantiles(values, n=100)[q - 1] * 1000, 2)
+ delivered = [b - a for a, b in zip(saved, seen)] # from Alice's answer to Bob's view
+ return {'sealed': sealed, 'lines': lines, 'saved_p50_ms': ms(saved, 50), 'saved_p99_ms': ms(saved, 99),
+ 'delivered_p50_ms': ms(delivered, 50), 'delivered_p99_ms': ms(delivered, 99),
+ 'shown_p50_ms': ms(seen, 50), 'shown_p90_ms': ms(seen, 90), 'shown_p99_ms': ms(seen, 99),
+ 'shown_max_ms': round(max(seen) * 1000, 2)}
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--aup', type=Path, default=ROOT.parent / 'aup' / 'build' / 'aup')
+ parser.add_argument('--vega', type=Path, default=ROOT.parent / 'vega')
+ parser.add_argument('--serve', type=Path, default=ROOT / 'build' / 'pure-serve')
+ parser.add_argument('--directory', type=Path, default=Path('/var/tmp'))
+ parser.add_argument('--lines', type=int, default=300)
+ parser.add_argument('--rounds', type=int, default=3)
+ args = parser.parse_args()
+ work = Path(tempfile.mkdtemp(prefix='pure-messenger-', dir=args.directory))
+ try:
+ module = work / 'messenger.pmod'
+ subprocess.run([str(args.aup), 'build', str(args.vega / 'examples' / 'messenger.aup'), '-o', str(module)],
+ env=dict(os.environ, AUP_PATH=str(args.vega / 'lib')), check=True)
+ for round_ in range(args.rounds):
+ for sealed in (True, False) if round_ % 2 == 0 else (False, True):
+ print(json.dumps({'round': round_, 'directory': str(args.directory),
+ **run(args.serve, module, work, sealed, args.lines)}), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()