aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/design/018-lifetimes-and-computation.md3
-rw-r--r--docs/research/117-transient-states.md53
-rw-r--r--research/experiments/032-transient-states/2026-10-04-ram.jsonl7
-rw-r--r--research/experiments/032-transient-states/2026-10-04-sd.jsonl7
-rwxr-xr-xresearch/experiments/032-transient-states/transient.py73
5 files changed, 142 insertions, 1 deletions
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()