aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-04 15:06:22 +0100
committereunhhu <[email protected]>2026-10-04 15:06:22 +0100
commit57ca99e5f4cdce7923e55347ed505e3ac43c3055 (patch)
treecff42b7ba8c26097a42758e672227353ececd005
parent611cadf4f4099eb9a84ab36ff72bee4e1caf05cf (diff)
downloadpure-57ca99e5f4cdce7923e55347ed505e3ac43c3055.tar.gz
pure-57ca99e5f4cdce7923e55347ed505e3ac43c3055.zip
Say what views that compute cost the writes beside them (018 stage 3, research 119)
-rw-r--r--docs/design/018-lifetimes-and-computation.md3
-rw-r--r--docs/research/119-views-that-compute.md82
-rw-r--r--research/experiments/034-view-bodies/2026-10-04-latency.jsonl25
-rw-r--r--research/experiments/034-view-bodies/2026-10-04-threads-1.jsonl4
-rw-r--r--research/experiments/034-view-bodies/2026-10-04-threads-2.jsonl4
-rw-r--r--research/experiments/034-view-bodies/2026-10-04-views-first.jsonl19
-rw-r--r--research/experiments/034-view-bodies/graph.aup79
-rwxr-xr-xresearch/experiments/034-view-bodies/views.py168
8 files changed, 383 insertions, 1 deletions
diff --git a/docs/design/018-lifetimes-and-computation.md b/docs/design/018-lifetimes-and-computation.md
index 00e5b52..d6dfe94 100644
--- a/docs/design/018-lifetimes-and-computation.md
+++ b/docs/design/018-lifetimes-and-computation.md
@@ -2,7 +2,8 @@
작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계(transient) 구현, 검사로 고정하고 쟀다
([117](../research/117-transient-states.md)). 2단계(핸들러의 계산) 구현, 검사로 고정하고 쟀다
-([118](../research/118-computing-in-one-event.md)). 3단계(보기의 본문) 구현, 검사로 고정했다.
+([118](../research/118-computing-in-one-event.md)). 3단계(보기의 본문) 구현, 검사로 고정하고 쟀다
+([119](../research/119-views-that-compute.md): 첫 통과 조건은 채우지 못했고, 다시 적은 조건은 채웠다).
[외부 리뷰 005](../outside-feedback/feedback-005.md)의 (1) "순수 인메모리 연산 및 알고리즘의 단절"과, 상태의 영속성과 상태성을 aup
안에서 정할 수단이 필요하다는 요청에서 시작한다.
diff --git a/docs/research/119-views-that-compute.md b/docs/research/119-views-that-compute.md
new file mode 100644
index 0000000..313fe10
--- /dev/null
+++ b/docs/research/119-views-that-compute.md
@@ -0,0 +1,82 @@
+# 119 — 계산하는 보기
+
+2026-10-04. [018](../design/018-lifetimes-and-computation.md)의 3단계(보기의 본문)를 잰다. 첫 기준은 구현하기 전에 적어
+커밋했다(`4d93488`). 첫 측정에서 그 하나가 처음부터 채울 수 없는 조건이었음을 보고, 다시 재기 전에 두 번째 기준을 적어
+커밋했다(`94dc1c7`).
+
+## 쉬운 말로
+
+- aup의 보기가 이제 계산한다: `view distance(start: u32, goal: u32) { … return … }`. 본문은 상태를 바꾸지 않고, 자기 `let`과
+ 자기 리스트만 바꾼다. 스냅숏을 읽는 연결 스레드에서 돌아서 소유자 스레드를 막지 않는다.
+- **같은 탐색을 이벤트로 하면 쓰기가 멈춘다(0.2%). 보기로 하면 쓰기는 계속된다.** 다만 탐색이 CPU를 쓰는 만큼 쓰기는 줄어든다:
+ 코어 셋에서 연결 둘이 쉬지 않고 탐색하면 쓰기가 30%로 준다. 같은 탐색을 다른 프로세스가 같은 코어에서 해도 69%로 준다.
+- **처음 적은 기준 하나는 채우지 못했다**: "보기를 묻는 동안 쓰기 처리량이 95% 이상". 쓰기가 이미 코어를 다 쓰고 있어서 처음부터
+ 채울 수 없는 조건이었다. 다시 적은 기준(쓰기의 p99가 CPU만 나눌 때의 2배를 넘지 않는다)은 채웠다(1.51배).
+- **연결 스레드가 막는 것이 꼬리에 남는다.** 보기를 계산하는 연결 스레드는 그동안 자기의 다른 연결에 답하지 못한다. 쓰기의 최대
+ 지연이 보기 하나의 길이(약 100–190 ms)까지 간다. 그리고 두 연결이 같은 연결 스레드에 떨어지면(커널이 정한다) 두 탐색이 차례로
+ 돈다: `--threads 2`에서 세 라운드 가운데 두 번이 그랬다. 그래서 "연결 스레드 둘에서 1.6배"는 중앙값으로 채우지 못했다(1.0배,
+ 흩어진 라운드에서는 2.0배).
+- 다음은 긴 보기를 연결 스레드 밖으로 넘기는 것이다.
+
+## 결과
+
+원시 결과와 명령은 [034-view-bodies](../../research/experiments/034-view-bodies/)에 있다(`views.py`, `graph.aup`).
+
+**방법.** `pure-serve`(연결 스레드 셋, 저장소는 RAM, 코어 0–2)에 `graph.aup`: 노드 2만 개, 간선 10만 개의 무작위 그래프(시드 119)와
+`distance` 보기(본문이 BFS)와 점수 입력(`score`). 부하 도구(코어 3)가 16연결로 점수를 10초 동안 보낸다. 넷을 번갈아 여섯 라운드:
+쓰기만(writes), 연결 둘이 같은 호스트의 `distance`를 쉬지 않고 물으며(both), 그 둘이 같은 코어의 다른 호스트에 물으며(apart: CPU만
+나눈다), 그 둘이 같은 탐색을 이벤트로 보내며(events: 소유자에서). 묻는 쪽은 미리 고른 출발 노드 64개에서 묻고, 모든 답을 그
+출발들로 미리 만든 모델과 견준다(첫 측정은 답을 볼 때 모델을 만들어 Python을 쟀다: 아래).
+
+| 중앙값 | 쓰기(초당) | 쓰기 p50 | 쓰기 p99 | 쓰기 최대 | 보기(초당) |
+|---|---:|---:|---:|---:|---:|
+| writes | 123,502 | 116 µs | 256 µs | 5.6 ms | — |
+| both | 36,809 (29.8%) | 152 µs | 1,232 µs | 98–188 ms | 43 또는 22 |
+| apart | 85,179 (69.0%) | 144 µs | 816 µs | 6–9 ms | 22 또는 40 |
+| events | 187 (0.2%) | 82 ms | 94 ms | 86–139 ms | 25 |
+
+- both와 apart는 라운드마다 크게 흔들린다(both 25,320–85,461, apart 54,757–90,794). 두 묻는 연결이 같은 연결 스레드에 떨어진 라운드는
+ 보기가 초당 22번(차례로), 다른 스레드에 떨어진 라운드는 43번(나란히)이고, 쓰기는 그 반대로 움직인다. CPU를 나누는 몫이 라운드마다
+ 다르다.
+- 쓰기 p99는 both가 apart의 1.51배다(두 번째 기준 2배 안). 최대 지연은 다르다: both는 보기 하나의 길이만큼, apart는 몇 ms다. 보기를
+ 계산하는 연결 스레드에 붙은 쓰기 연결이 그동안 기다린다. 그런 쓰기는 1% 미만이다.
+- events에서는 탐색 하나(약 80 ms)마다 소유자가 쓰기를 받지 못한다.
+
+**연결 스레드의 수.** 묻는 연결 둘, 각각 세 라운드(`threads-1`, `threads-2`).
+
+| | 라운드 | 중앙값 |
+|---|---|---:|
+| `--threads 1` | 23.6, 23.6, 23.7 | 23.6 |
+| `--threads 2` | 46.8, 23.7, 23.7 | 23.7 (1.0배) |
+| `--threads 3`(위의 사전 측정, 한 라운드) | 묻는 연결 하나 23.2, 둘 46.4 | 2.0배 |
+
+`--threads 2`의 둘째와 셋째 라운드에서 두 연결은 같은 스레드에 떨어졌다(`SO_REUSEPORT`가 연결을 나눈다). 보기 하나는 약 43 ms다.
+
+**첫 측정**(`2026-10-04-views-first.jsonl`). 쓰기는 44.6%, apart 46.0%, 보기는 묻는 연결 하나 13.5, 둘 23.3(1.73배)이었다. 묻는 쪽이
+답마다 새 출발의 모델을 Python으로 만들어서(약 30 ms), 보기의 처리량이 아니라 Python을 쟀다. 쓰기를 줄인 탐색의 양도 그만큼
+적었다. 결과는 두 번째 측정(위)을 쓴다.
+
+**검사.**
+- `test-module`의 `view_bodies`: 본문의 지역 칸과 세는 반복, 걷기로 채우는 키 있는 이벤트의 리스트가 평가마다 새로 시작한다(앱에서
+ 두 번). 끝나지 않는 반복은 `work`, 예산을 넘는 리스트는 `memory`로 값이 없고 상태는 그대로다. 한 스냅숏을 스레드 셋이 2,000번씩
+ 동시에 읽어도 답이 맞고 저마다의 리스트가 비어서 끝난다. 상태를 쓰는 본문, 저장되는 리스트에 넣는 본문, 반복 밖의 `break`는 로드가
+ 거절한다.
+- Pure `tests/check.py`의 `server_view_bodies`: aup의 `routes.aup`를 띄워, 64연결이 동시에 무작위 인자로 물은 `distance` 1,280개가
+ 모두 모델과 같다. `reach`(인자 없는 본문)가 모델과 같다. `--work 2000`이면 422 `work`, `--memory 2000`이면 503 `memory`다.
+- aup `tests/check.py`: `computing`의 `reach`가 무작위 그래프 20개의 모든 단계에서 모델과 같다. 진단 8건.
+
+## 판정
+
+- **018의 3단계는 들어간다.** 첫 통과 조건(쓰기 95%)은 채우지 못했다. 그 조건은 쓰기가 코어를 다 쓰는 동안에는 채울 수 없었다.
+ 두 번째 조건(쓰기 p99, CPU만 나누는 것의 2배 안)은 채웠다. 들이는 근거는 018에 적은 대안과의 비교다: 같은 계산을 핸들러에서
+ 하면 쓰기가 0.2%로 멈춘다. 보기로 하면 CPU를 나누는 만큼만 준다.
+- 연결 스레드 수의 기준(둘에서 1.6배)은 중앙값으로 채우지 못했다. 보기가 연결 스레드에서 돌고, 연결이 어느 스레드에 떨어질지는
+ 커널이 정하기 때문이다.
+
+## 다음
+
+- **긴 보기를 연결 스레드 밖으로.** 본문이 있는 보기(또는 실행량이 큰 보기)를 연결 스레드가 아니라 보기의 스레드들에 넘겨 계산하고
+ 답만 돌려받는다. 그러면 연결 스레드가 막는 꼬리(쓰기 최대 지연)가 없어지고, 보기는 연결의 자리와 상관없이 나란히 돈다. 기준을
+ 적고 잰다.
+- 다음 작업인 SSE(보기의 변화를 미는 것)는 바뀔 때마다 보기를 다시 계산한다. 본문이 있는 보기를 미는 비용은 위의 보기의 스레드와
+ 함께 다룬다.
diff --git a/research/experiments/034-view-bodies/2026-10-04-latency.jsonl b/research/experiments/034-view-bodies/2026-10-04-latency.jsonl
new file mode 100644
index 0000000..d074e8f
--- /dev/null
+++ b/research/experiments/034-view-bodies/2026-10-04-latency.jsonl
@@ -0,0 +1,25 @@
+{"round": 1, "way": "writes", "writes_per_second": 120607, "write_p50_us": 120, "write_p99_us": 272, "write_max_us": 7168, "views_per_second": 0.0}
+{"round": 1, "way": "both", "writes_per_second": 43296, "write_p50_us": 144, "write_p99_us": 640, "write_max_us": 98304, "views_per_second": 43.0}
+{"round": 1, "way": "apart", "writes_per_second": 88452, "write_p50_us": 144, "write_p99_us": 864, "write_max_us": 7680, "views_per_second": 21.8}
+{"round": 1, "way": "events", "writes_per_second": 187, "write_p50_us": 81920, "write_p99_us": 98304, "write_max_us": 139264, "views_per_second": 24.6}
+{"round": 2, "way": "events", "writes_per_second": 187, "write_p50_us": 81920, "write_p99_us": 94208, "write_max_us": 98304, "views_per_second": 24.6}
+{"round": 2, "way": "apart", "writes_per_second": 88993, "write_p50_us": 144, "write_p99_us": 704, "write_max_us": 6144, "views_per_second": 21.6}
+{"round": 2, "way": "both", "writes_per_second": 30321, "write_p50_us": 152, "write_p99_us": 1472, "write_max_us": 102400, "views_per_second": 43.1}
+{"round": 2, "way": "writes", "writes_per_second": 125767, "write_p50_us": 116, "write_p99_us": 232, "write_max_us": 5632, "views_per_second": 0.0}
+{"round": 3, "way": "writes", "writes_per_second": 123756, "write_p50_us": 116, "write_p99_us": 272, "write_max_us": 5120, "views_per_second": 0.0}
+{"round": 3, "way": "both", "writes_per_second": 85461, "write_p50_us": 108, "write_p99_us": 368, "write_max_us": 188416, "views_per_second": 22.3}
+{"round": 3, "way": "apart", "writes_per_second": 54757, "write_p50_us": 232, "write_p99_us": 1984, "write_max_us": 9216, "views_per_second": 39.6}
+{"round": 3, "way": "events", "writes_per_second": 188, "write_p50_us": 81920, "write_p99_us": 94208, "write_max_us": 98304, "views_per_second": 24.5}
+{"round": 4, "way": "events", "writes_per_second": 186, "write_p50_us": 81920, "write_p99_us": 86016, "write_max_us": 86016, "views_per_second": 24.7}
+{"round": 4, "way": "apart", "writes_per_second": 90794, "write_p50_us": 144, "write_p99_us": 672, "write_max_us": 6400, "views_per_second": 21.9}
+{"round": 4, "way": "both", "writes_per_second": 29831, "write_p50_us": 152, "write_p99_us": 1664, "write_max_us": 155648, "views_per_second": 43.0}
+{"round": 4, "way": "writes", "writes_per_second": 123193, "write_p50_us": 116, "write_p99_us": 240, "write_max_us": 5632, "views_per_second": 0.0}
+{"round": 5, "way": "writes", "writes_per_second": 123248, "write_p50_us": 116, "write_p99_us": 288, "write_max_us": 5888, "views_per_second": 0.0}
+{"round": 5, "way": "both", "writes_per_second": 25320, "write_p50_us": 152, "write_p99_us": 2432, "write_max_us": 163840, "views_per_second": 43.1}
+{"round": 5, "way": "apart", "writes_per_second": 81905, "write_p50_us": 160, "write_p99_us": 768, "write_max_us": 6912, "views_per_second": 21.8}
+{"round": 5, "way": "events", "writes_per_second": 185, "write_p50_us": 81920, "write_p99_us": 90112, "write_max_us": 94208, "views_per_second": 24.7}
+{"round": 6, "way": "events", "writes_per_second": 184, "write_p50_us": 81920, "write_p99_us": 94208, "write_max_us": 102400, "views_per_second": 24.5}
+{"round": 6, "way": "apart", "writes_per_second": 55420, "write_p50_us": 232, "write_p99_us": 1664, "write_max_us": 8192, "views_per_second": 40.9}
+{"round": 6, "way": "both", "writes_per_second": 47134, "write_p50_us": 160, "write_p99_us": 992, "write_max_us": 118784, "views_per_second": 42.6}
+{"round": 6, "way": "writes", "writes_per_second": 125222, "write_p50_us": 116, "write_p99_us": 240, "write_max_us": 6144, "views_per_second": 0.0}
+{"nodes": 20000, "edges": 100000, "threads": 3, "askers": 2, "medians": {"writes": 123502.0, "both": 36808.5, "apart": 85178.5, "events": 186.5}, "write_p99_medians_us": {"writes": 256.0, "both": 1232.0, "apart": 816.0, "events": 94208.0}, "writes_kept": {"both": 29.8, "apart": 69.0, "events": 0.2}, "p99_both_over_apart": 1.51}
diff --git a/research/experiments/034-view-bodies/2026-10-04-threads-1.jsonl b/research/experiments/034-view-bodies/2026-10-04-threads-1.jsonl
new file mode 100644
index 0000000..1a52ac0
--- /dev/null
+++ b/research/experiments/034-view-bodies/2026-10-04-threads-1.jsonl
@@ -0,0 +1,4 @@
+{"round": 1, "way": "many", "views_per_second": 23.6}
+{"round": 2, "way": "many", "views_per_second": 23.6}
+{"round": 3, "way": "many", "views_per_second": 23.7}
+{"nodes": 20000, "edges": 100000, "threads": 1, "askers": 2, "medians": {"many": 23.6}, "write_p99_medians_us": {}}
diff --git a/research/experiments/034-view-bodies/2026-10-04-threads-2.jsonl b/research/experiments/034-view-bodies/2026-10-04-threads-2.jsonl
new file mode 100644
index 0000000..4034b85
--- /dev/null
+++ b/research/experiments/034-view-bodies/2026-10-04-threads-2.jsonl
@@ -0,0 +1,4 @@
+{"round": 1, "way": "many", "views_per_second": 46.8}
+{"round": 2, "way": "many", "views_per_second": 23.7}
+{"round": 3, "way": "many", "views_per_second": 23.7}
+{"nodes": 20000, "edges": 100000, "threads": 2, "askers": 2, "medians": {"many": 23.7}, "write_p99_medians_us": {}}
diff --git a/research/experiments/034-view-bodies/2026-10-04-views-first.jsonl b/research/experiments/034-view-bodies/2026-10-04-views-first.jsonl
new file mode 100644
index 0000000..abfdd45
--- /dev/null
+++ b/research/experiments/034-view-bodies/2026-10-04-views-first.jsonl
@@ -0,0 +1,19 @@
+{"round": 1, "way": "writes", "writes_per_second": 117606, "views_per_second": 0.0}
+{"round": 1, "way": "both", "writes_per_second": 52913, "views_per_second": 16.1}
+{"round": 1, "way": "apart", "writes_per_second": 53391, "views_per_second": 27.8}
+{"round": 1, "way": "events", "writes_per_second": 189, "views_per_second": 24.9}
+{"round": 1, "way": "one", "views_per_second": 13.3}
+{"round": 1, "way": "many", "views_per_second": 22.2}
+{"round": 2, "way": "many", "views_per_second": 23.9}
+{"round": 2, "way": "one", "views_per_second": 13.5}
+{"round": 2, "way": "events", "writes_per_second": 193, "views_per_second": 24.8}
+{"round": 2, "way": "apart", "writes_per_second": 54521, "views_per_second": 33.6}
+{"round": 2, "way": "both", "writes_per_second": 81288, "views_per_second": 22.4}
+{"round": 2, "way": "writes", "writes_per_second": 118522, "views_per_second": 0.0}
+{"round": 3, "way": "writes", "writes_per_second": 119471, "views_per_second": 0.0}
+{"round": 3, "way": "both", "writes_per_second": 27513, "views_per_second": 37.6}
+{"round": 3, "way": "apart", "writes_per_second": 89647, "views_per_second": 21.6}
+{"round": 3, "way": "events", "writes_per_second": 188, "views_per_second": 24.6}
+{"round": 3, "way": "one", "views_per_second": 13.7}
+{"round": 3, "way": "many", "views_per_second": 23.3}
+{"nodes": 20000, "edges": 100000, "threads": 3, "askers": 2, "writes_median": 118522, "with_views_median": 52913, "writes_kept": 44.6, "writes_kept_apart": 46.0, "writes_kept_events": 0.2, "views_one": 13.5, "views_many": 23.3, "scaling": 1.73}
diff --git a/research/experiments/034-view-bodies/graph.aup b/research/experiments/034-view-bodies/graph.aup
new file mode 100644
index 0000000..41302d9
--- /dev/null
+++ b/research/experiments/034-view-bodies/graph.aup
@@ -0,0 +1,79 @@
+# The app of research 119 (docs/design/018 stage 3): a graph whose
+# distances a view computes in its body, beside the scoreboard's write
+# (benchmarks/http/load's score) that the owner applies.
+record Edge {
+ link = 0
+ to = 0
+}
+record Seen {
+ node = 0
+ depth = 0
+}
+record Step {
+ node = 0
+}
+state edges: list of Edge by link
+state nodes = 0
+state points = 0
+input size = message {
+ nodes: u32
+}
+input load = message {
+ rows: list of Edge
+}
+input score = message {
+ points: u32
+ name: bytes
+}
+input route = message {
+ start: u32
+ goal: u32
+}
+on size {
+ nodes = size.nodes
+}
+on load {
+ for r in load.rows {
+ edges.add(Edge { ..r })
+ }
+}
+on score {
+ points = points + score.points
+}
+# The same search as an event, on the owner (to compare).
+on route {
+ let seen: list of Seen by node
+ let queue: list of Step
+ queue.append(Step { node: route.start })
+ seen.add(Seen { node: route.start })
+ for i in 0 .. nodes {
+ if i >= queue.count { break }
+ let q = queue.first + i
+ for e in edges[q.node * 1048576 .. q.node * 1048576 + 1048576] {
+ if !seen.has(e.to) {
+ seen.add(Seen { node: e.to, depth: seen[q.node].depth + 1 })
+ queue.append(Step { node: e.to })
+ }
+ }
+ }
+ hops = if seen.has(route.goal) { seen[route.goal].depth } else { -1 }
+}
+state hops = -1
+view total = points
+view distance(start: u32, goal: u32) {
+ let seen: list of Seen by node
+ let queue: list of Step
+ queue.append(Step { node: start })
+ seen.add(Seen { node: start })
+ for i in 0 .. nodes {
+ if i >= queue.count { break }
+ let q = queue.first + i
+ for e in edges[q.node * 1048576 .. q.node * 1048576 + 1048576] {
+ if !seen.has(e.to) {
+ seen.add(Seen { node: e.to, depth: seen[q.node].depth + 1 })
+ queue.append(Step { node: e.to })
+ }
+ }
+ }
+ return if seen.has(goal) { seen[goal].depth } else { -1 }
+}
diff --git a/research/experiments/034-view-bodies/views.py b/research/experiments/034-view-bodies/views.py
new file mode 100755
index 0000000..92bdc7c
--- /dev/null
+++ b/research/experiments/034-view-bodies/views.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+"""Views that compute, beside writes (docs/design/018 stage 3, research
+119). pure-serve (THREADS connection threads, store in RAM, cores 0-2)
+holds graph.aup with a random graph of NODES nodes and EDGES edges (seed
+119); a second one like it (apart) shares the cores. In turns, for SECONDS
+each, ROUNDS rounds:
+ writes benchmarks/http/load posts points (16 connections, keep-alive,
+ core 3): the owner applies them
+ both the same, while ASKERS connections ask the distance view (its
+ body a breadth first search) without pause, random arguments
+ apart the same, the askers asking the second host: the cores shared,
+ not the host
+ events the same, the askers posting the search as events (route): on
+ the owner
+ one, many the askers alone, 1 and ASKERS of them
+Every view answer is checked against a model, made before any run is
+timed: the askers start from 64 nodes picked beforehand (a model made while
+asking would time Python, not the host).
+
+views.py PURE_SERVE AUP NODES EDGES THREADS ASKERS SECONDS ROUNDS [WAYS] a JSON line per run, then medians
+(WAYS: the ways to run, by commas; all by default)
+"""
+import http.client
+import json
+import random
+import shutil
+import statistics
+import struct
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+LOAD = HERE.parents[2] / 'benchmarks' / 'http' / 'load'
+
+
+def main():
+ serve, aup = Path(sys.argv[1]).resolve(), Path(sys.argv[2]).resolve()
+ nodes, edges_wanted, threads, askers, seconds, rounds = (int(a) for a in sys.argv[3:9])
+ chosen = sys.argv[9].split(',') if len(sys.argv) > 9 else ['writes', 'both', 'apart', 'events', 'one', 'many']
+ rng = random.Random(119)
+ pairs = set()
+ while len(pairs) < edges_wanted:
+ a, b = rng.randrange(nodes), rng.randrange(nodes)
+ if a != b:
+ pairs.add((a, b))
+ edges = sorted(pairs)
+ out = [[] for _ in range(nodes)]
+ for a, b in edges:
+ out[a].append(b)
+ model = {}
+
+ def far(a, b):
+ if a not in model:
+ depth, frontier = {a: 0}, [a]
+ for v in frontier:
+ for t in out[v]:
+ if t not in depth:
+ depth[t] = depth[v] + 1
+ frontier.append(t)
+ model[a] = depth
+ return model[a].get(b, -1)
+
+ work = Path(tempfile.mkdtemp(prefix='pure-034-', dir='/dev/shm'))
+ try:
+ subprocess.run([str(aup), 'build', str(HERE / 'graph.aup'), '-o', str(work / 'graph.pmod')], check=True)
+ host = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--module', str(work / 'graph.pmod'), '--store',
+ str(work / 'store'), '--port', '0', '--threads', str(threads)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ port = json.loads(host.stdout.readline())['port']
+ apart = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--module', str(work / 'graph.pmod'), '--store',
+ str(work / 'apart'), '--port', '0', '--threads', str(threads)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ apart_port = json.loads(apart.stdout.readline())['port']
+ for target in (port, apart_port):
+ setup = http.client.HTTPConnection('127.0.0.1', target, timeout=600)
+
+ def post(path, body=b''):
+ setup.request('POST', path, body, {'Content-Length': str(len(body))})
+ answer = setup.getresponse()
+ assert answer.status == 200 and answer.read() is not None, path
+
+ post(f'/inputs/size?nodes={nodes}')
+ for k in range(0, len(edges), 80000):
+ post('/inputs/load', b''.join(struct.pack('<dd', a * 1048576 + b, b) for a, b in edges[k:k + 80000]))
+
+ starts = [rng.randrange(nodes) for _ in range(64)]
+ for a in starts:
+ far(a, a)
+
+ def ask(target, until, seed, done, event=False):
+ r = random.Random(seed)
+ c = http.client.HTTPConnection('127.0.0.1', target, timeout=600)
+ while time.monotonic() < until:
+ a, b = r.choice(starts), r.randrange(nodes)
+ if event:
+ c.request('POST', f'/inputs/route?start={a}&goal={b}', b'', {'Content-Length': '0'})
+ else:
+ c.request('GET', f'/views/distance?start={a}&goal={b}')
+ answer = c.getresponse()
+ body = answer.read()
+ assert answer.status == 200 and (event or int(body) == far(a, b)), (a, b, answer.status, body)
+ done.append(1)
+
+ def views(count):
+ done, until = [], time.monotonic() + seconds
+ ts = [threading.Thread(target=ask, args=(port, until, k * 7919 + len(model), done)) for k in range(count)]
+ for t in ts:
+ t.start()
+ for t in ts:
+ t.join()
+ return len(done) / seconds
+
+ def writes(way):
+ done, ts = [], []
+ if way != 'writes':
+ until = time.monotonic() + seconds + 0.5
+ ts = [threading.Thread(target=ask, args=(apart_port if way == 'apart' else port, until, k * 104729, done,
+ way == 'events')) for k in range(askers)]
+ for t in ts:
+ t.start()
+ got = subprocess.run(['taskset', '-c', '3', str(LOAD), str(port), '16', str(seconds), 'score', '64', 'keep-alive'],
+ capture_output=True, text=True, check=True).stdout
+ for t in ts:
+ t.join()
+ result = json.loads(got)
+ assert result['2xx'] == result['completed'] and not result['errors'], result
+ return result, len(done) / seconds
+
+ rates, p99 = {}, {}
+ for round_ in range(1, rounds + 1):
+ ways = chosen
+ for way in (ways if round_ % 2 else ways[::-1]):
+ if way in ('writes', 'both', 'apart', 'events'):
+ result, v = writes(way)
+ rates.setdefault(way, []).append(result['requests_per_second'])
+ p99.setdefault(way, []).append(result['p99_us'])
+ line = {'round': round_, 'way': way, 'writes_per_second': result['requests_per_second'],
+ 'write_p50_us': result['p50_us'], 'write_p99_us': result['p99_us'],
+ 'write_max_us': result['max_us'], 'views_per_second': round(v, 1)}
+ else:
+ v = views(1 if way == 'one' else askers)
+ rates.setdefault(way, []).append(v)
+ line = {'round': round_, 'way': way, 'views_per_second': round(v, 1)}
+ print(json.dumps(line), flush=True)
+ for h in (host, apart):
+ h.terminate()
+ h.communicate(timeout=60)
+ med = {k: statistics.median(v) for k, v in rates.items()}
+ tail = {k: statistics.median(v) for k, v in p99.items()}
+ summary = {'nodes': nodes, 'edges': len(edges), 'threads': threads, 'askers': askers,
+ 'medians': {k: round(v, 1) for k, v in med.items()}, 'write_p99_medians_us': tail}
+ if 'writes' in med:
+ summary['writes_kept'] = {k: round(med[k] / med['writes'] * 100, 1) for k in ('both', 'apart', 'events') if k in med}
+ if 'both' in tail and 'apart' in tail:
+ summary['p99_both_over_apart'] = round(tail['both'] / tail['apart'], 2)
+ if 'one' in med and 'many' in med:
+ summary['scaling'] = round(med['many'] / med['one'], 2)
+ print(json.dumps(summary), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()