From 09a34871f97c9917735b4dfd40cbad22ccf9b001 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 4 Oct 2026 13:33:01 +0100 Subject: Say what a transient state saves a write (018 stage 1, research 117) --- docs/design/018-lifetimes-and-computation.md | 3 +- docs/research/117-transient-states.md | 53 ++++++++++++++++ .../032-transient-states/2026-10-04-ram.jsonl | 7 +++ .../032-transient-states/2026-10-04-sd.jsonl | 7 +++ .../experiments/032-transient-states/transient.py | 73 ++++++++++++++++++++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 docs/research/117-transient-states.md create mode 100644 research/experiments/032-transient-states/2026-10-04-ram.jsonl create mode 100644 research/experiments/032-transient-states/2026-10-04-sd.jsonl create mode 100755 research/experiments/032-transient-states/transient.py diff --git a/docs/design/018-lifetimes-and-computation.md b/docs/design/018-lifetimes-and-computation.md index fab6d96..e25973c 100644 --- a/docs/design/018-lifetimes-and-computation.md +++ b/docs/design/018-lifetimes-and-computation.md @@ -1,6 +1,7 @@ # 018 — 상태의 수명과 메모리 안의 계산 -작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계(transient) 구현, 검사로 고정했다. +작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계(transient) 구현, 검사로 고정하고 쟀다 +([117](../research/117-transient-states.md)). [외부 리뷰 005](../outside-feedback/feedback-005.md)의 (1) "순수 인메모리 연산 및 알고리즘의 단절"과, 상태의 영속성과 상태성을 aup 안에서 정할 수단이 필요하다는 요청에서 시작한다. diff --git a/docs/research/117-transient-states.md b/docs/research/117-transient-states.md new file mode 100644 index 0000000..5e5fe12 --- /dev/null +++ b/docs/research/117-transient-states.md @@ -0,0 +1,53 @@ +# 117 — 저장하지 않는 상태 + +2026-10-04. [018](../design/018-lifetimes-and-computation.md)의 1단계(`transient state`)를 잰다. 1단계의 기준은 검사였고(018에 +구현 전에 적어 커밋했다, `4d93488`), 처리량은 기준 없이 밝히기로 했다. + +## 쉬운 말로 + +- aup에서 `transient state`로 선언한 상태는 앱이 도는 동안만 있다. 다시 시작하면 초기값이고, 저장소에는 아무것도 쓰지 않는다. + 실패한 이벤트에서 되돌리는 것, 보기에서 읽는 것, 메모리 예산에 드는 것은 다른 상태와 같다. +- SD 카드에 저장소를 둔 점수판에서 저장하는 쓰기는 초당 7,598번이고, 같은 쓰기를 transient 상태에 하면 초당 132,305번이다 + (17배). 저장하는 쓰기는 요청마다 장치에 약 1 KB를 쓰고, transient 쓰기는 0바이트를 쓴다. +- 저장소가 RAM에 있으면 둘은 거의 같다(+1.5%, 흔들림 안). 저장이 원래 싸기 때문이다. + +## 결과 + +원시 결과와 명령은 [032-transient-states](../../research/experiments/032-transient-states/)에 있다(`transient.py`). + +**방법.** 한 호스트(`--apps`, 연결 스레드 하나, 실행 스레드 둘, 코어 0–2)에 점수판(`benchmarks/http/scores.aup`) 둘: `stored` +(그대로)와 `transient`(`board`가 transient 상태). 부하 도구(코어 3)가 16연결 keep-alive로 10초 동안 점수를 올린다(키 64개). +둘을 번갈아 세 라운드다. 장치에 쓴 바이트는 호스트의 `/proc/PID/io`의 `write_bytes` 차이다. + +| 중앙값 | stored | transient | +|---|---:|---:| +| RAM(`/dev/shm`), 초당 요청 | 130,576 | 132,522 (+1.5%) | +| SD 카드(`~/.cache`, ext4), 초당 요청 | 7,598 | 132,305 (17.4배) | +| SD 카드, 요청마다 장치에 쓴 바이트 | 1,033 | 0 | + +SD의 저장하는 쓰기는 p50 1.5 ms, p99 7.2 ms이고 transient는 p50 116 µs, p99 168 µs다. RAM의 두 앱과 같다. + +**검사.** +- `test-module`의 `transient_states`: 수, text, 리스트, 인덱스가 transient인 앱이 다시 시작하면 초기값이고 저장되는 상태는 그대로다. + 실패한 이벤트는 transient도 되돌린다. transient만 바꾸는 이벤트는 저장 번호를 올리지 않는다. 스냅숏은 transient text를 + 복사해 읽는 쪽에 보인다. 모듈을 바꿔 저장되던 것을 transient로, transient를 저장되는 것으로 바꾸면 다시 시작한 뒤에 바뀐 + 대로다. 저장되지 않게 된 리스트를 다시 저장하면 저장소의 그 리스트는 저장되지 않게 된 때의 것이다(버린 상태를 다시 들인 것과 + 같다). transient 리스트를 가리키는 저장되는 인덱스를 가진 모듈은 로드가 거절한다. +- Pure `tests/check.py`의 `server_transient`: `pure-serve`에서 같은 저장 요청 다섯 사이에 transient만 바꾸는 요청 100개(수, + 리스트, text)를 섞어도 저장소의 파일과 바이트가 섞지 않은 것과 같다. 그 요청 100개는 저장소의 어떤 파일도 바꾸지 않는다. + transient text를 바꾼 직후 보기(스냅숏에서 읽는다)가 새 값을 답한다. `kill -9` 뒤에 다시 띄우면 transient는 초기값이고 저장되는 + 상태는 마지막 요청까지 있다. +- aup `tests/check.py`의 `transients`와 진단 5건: 같은 앱의 transient 판과 저장 판의 모듈이 상태 타입 워드 세 바이트만 다르다. + `pure-run --store`에서 위와 같은 저장소 비교, 다시 열기, 모듈 교체. + +## 판정 + +- **018의 1단계는 들어간다.** 상태의 영속성은 이제 aup의 선언이 정한다: `state`(저장), `transient state`(앱이 도는 동안), + `let`(이벤트 동안). +- 저장소가 느린 기기에서 재시작 뒤에 뜻이 없는 상태(접속 수, 캐시, 진행 중 표시)를 transient로 두면 그 쓰기가 저장의 값을 내지 + 않는다. + +## 다음 + +- 018의 2단계: 핸들러의 계산(이벤트의 리스트, 세는 반복, `break`, 중첩, 기본 예산). +- transient text는 스냅숏마다 복사한다. 큰 transient text가 필요해지면 복사본을 스냅숏 사이에 나누는 것을 잰다. diff --git a/research/experiments/032-transient-states/2026-10-04-ram.jsonl b/research/experiments/032-transient-states/2026-10-04-ram.jsonl new file mode 100644 index 0000000..0a87923 --- /dev/null +++ b/research/experiments/032-transient-states/2026-10-04-ram.jsonl @@ -0,0 +1,7 @@ +{"medium": "ram", "round": 1, "way": "stored", "written_per_request": 0.0, "requests_per_second": 132446, "completed": 1324463, "2xx": 1324463, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 144, "max_us": 6656} +{"medium": "ram", "round": 1, "way": "transient", "written_per_request": 0.0, "requests_per_second": 131252, "completed": 1312517, "2xx": 1312517, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 152, "max_us": 8192} +{"medium": "ram", "round": 2, "way": "transient", "written_per_request": 0.0, "requests_per_second": 132522, "completed": 1325225, "2xx": 1325225, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 144, "max_us": 4608} +{"medium": "ram", "round": 2, "way": "stored", "written_per_request": 0.0, "requests_per_second": 128044, "completed": 1280441, "2xx": 1280441, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 120, "p99_us": 184, "max_us": 6144} +{"medium": "ram", "round": 3, "way": "stored", "written_per_request": 0.0, "requests_per_second": 130576, "completed": 1305763, "2xx": 1305763, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 120, "p99_us": 152, "max_us": 5888} +{"medium": "ram", "round": 3, "way": "transient", "written_per_request": 0.0, "requests_per_second": 133163, "completed": 1331634, "2xx": 1331634, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 144, "max_us": 3968} +{"medium": "ram", "stored_median": 130576, "transient_median": 132522, "change": 1.5, "written_per_request": {"stored": 0.0, "transient": 0.0}} diff --git a/research/experiments/032-transient-states/2026-10-04-sd.jsonl b/research/experiments/032-transient-states/2026-10-04-sd.jsonl new file mode 100644 index 0000000..0f55fbe --- /dev/null +++ b/research/experiments/032-transient-states/2026-10-04-sd.jsonl @@ -0,0 +1,7 @@ +{"medium": "sd", "round": 1, "way": "stored", "written_per_request": 1032.9, "requests_per_second": 7598, "completed": 75981, "2xx": 75981, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 1600, "p99_us": 7168, "max_us": 59392} +{"medium": "sd", "round": 1, "way": "transient", "written_per_request": 0.0, "requests_per_second": 129617, "completed": 1296169, "2xx": 1296169, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 168, "max_us": 11776} +{"medium": "sd", "round": 2, "way": "transient", "written_per_request": 0.0, "requests_per_second": 132888, "completed": 1328882, "2xx": 1328882, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 136, "max_us": 5120} +{"medium": "sd", "round": 2, "way": "stored", "written_per_request": 1032.9, "requests_per_second": 7513, "completed": 75137, "2xx": 75137, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 1536, "p99_us": 7168, "max_us": 106496} +{"medium": "sd", "round": 3, "way": "stored", "written_per_request": 1033.4, "requests_per_second": 7601, "completed": 76193, "2xx": 76193, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 1536, "p99_us": 7168, "max_us": 59392} +{"medium": "sd", "round": 3, "way": "transient", "written_per_request": 0.0, "requests_per_second": 132305, "completed": 1323051, "2xx": 1323051, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 116, "p99_us": 152, "max_us": 4096} +{"medium": "sd", "stored_median": 7598, "transient_median": 132305, "change": 1641.3, "written_per_request": {"stored": 1032.9, "transient": 0.0}} diff --git a/research/experiments/032-transient-states/transient.py b/research/experiments/032-transient-states/transient.py new file mode 100755 index 0000000..87153c8 --- /dev/null +++ b/research/experiments/032-transient-states/transient.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""What a transient state saves (docs/design/018 stage 1, research 117). A +host of apps (one connection thread, two workers, cores 0-2) holds the +scoreboard (benchmarks/http/scores.aup) twice: stored, as it is, and +transient, its board a transient state. The load (benchmarks/http/load, +core 3) posts points for SECONDS on CONNECTIONS connections, keep-alive, to +one of them; the two in turns, their order turned each round. The stores +are in RAM (/dev/shm) or on the SD card (~/.cache). Each run says what the +host wrote to the device meanwhile (/proc/PID/io write_bytes). + +transient.py PURE_SERVE ram|sd SECONDS ROUNDS a JSON line per run, then medians +""" +import json +import shutil +import statistics +import subprocess +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +AUP = ROOT.parent / 'aup' / 'build' / 'aup' +LOAD = ROOT / 'benchmarks' / 'http' / 'load' + + +def written(pid): + for line in Path(f'/proc/{pid}/io').read_text().splitlines(): + if line.startswith('write_bytes:'): + return int(line.split()[1]) + + +def main(): + serve, medium, seconds, rounds = Path(sys.argv[1]).resolve(), sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) + base = Path('/dev/shm') if medium == 'ram' else Path.home() / '.cache' + base.mkdir(parents=True, exist_ok=True) + work = Path(tempfile.mkdtemp(prefix='pure-032-', dir=base)) + try: + source = (ROOT / 'benchmarks' / 'http' / 'scores.aup').read_text() + (work / 'transient.aup').write_text(source.replace('state board', 'transient state board')) + for name, path in (('stored', ROOT / 'benchmarks' / 'http' / 'scores.aup'), ('transient', work / 'transient.aup')): + subprocess.run([str(AUP), 'build', str(path), '-o', str(work / f'{name}.pmod')], check=True) + (work / 'apps.conf').write_text(f"stored {work / 'stored.pmod'} {work / 'stored'}\n" + f"transient {work / 'transient.pmod'} {work / 'transient'}\n") + host = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--apps', str(work / 'apps.conf'), '--port', '0', + '--threads', '1', '--workers', '2'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + port = json.loads(host.stdout.readline())['port'] + rates, bytes_ = {}, {} + for round_ in range(1, rounds + 1): + ways = ['stored', 'transient'] + for way in (ways if round_ % 2 else ways[::-1]): + before = written(host.pid) + out = subprocess.run(['taskset', '-c', '3', str(LOAD), str(port), '16', str(seconds), 'score', '64', + 'keep-alive', '16', f'/{way}'], capture_output=True, text=True, check=True).stdout + result = json.loads(out) + assert result['2xx'] == result['completed'] and not result['errors'], result + per = (written(host.pid) - before) / result['completed'] + rates.setdefault(way, []).append(result['requests_per_second']) + bytes_.setdefault(way, []).append(per) + print(json.dumps({'medium': medium, 'round': round_, 'way': way, 'written_per_request': round(per, 1), + **result}), flush=True) + host.terminate() + host.communicate(timeout=60) + stored, transient = statistics.median(rates['stored']), statistics.median(rates['transient']) + print(json.dumps({'medium': medium, 'stored_median': stored, 'transient_median': transient, + 'change': round((transient / stored - 1) * 100, 1), + 'written_per_request': {w: round(statistics.median(bytes_[w]), 1) for w in bytes_}}), flush=True) + finally: + shutil.rmtree(work) + + +if __name__ == '__main__': + main() -- cgit v1.2.3