aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/research/073-single-file-persistent-tree.md10
-rw-r--r--docs/research/074-runtime-owned-drafts.md97
2 files changed, 102 insertions, 5 deletions
diff --git a/docs/research/073-single-file-persistent-tree.md b/docs/research/073-single-file-persistent-tree.md
index e2a2ee9..092b6ad 100644
--- a/docs/research/073-single-file-persistent-tree.md
+++ b/docs/research/073-single-file-persistent-tree.md
@@ -116,11 +116,11 @@ TUI의 실제 PTY 검사(1 MiB, UTF-8, 충돌, 1,024개 문서 목록)다. 기�
## 다음 결정
-TUI의 17 ms 중 저장소 자체는 약 8.5 ms다. 나머지는 저장본 전체를 연속 뷰로 다시 읽어
-초안과 비교하는 "초안 대 저장본 비교" 관례의 비용이다. 편집기는 무엇을 바꿨는지 이미 알고 있다.
+TUI의 17 ms 중 저장소 자체는 약 8.5 ms다. 처음에는 나머지 전부를 저장본을 다시 읽어 초안과
+비교하는 비용으로 추정했으나 틀렸다. [074](074-runtime-owned-drafts.md)에서 저장소 API 수준으로
+재 보니 그 비용은 약 1–2 ms였다. 나머지는 화면 갱신·PTY·런타임 경로이며 아직 분해하지 않았다.
-1. 편집 연산을 그대로 전달하고, 초안을 같은 트리의 커밋되지 않은 루트로 둔다. 그러면
- 전체 비교, 전체 복사, 별도 gap buffer가 사라지는지 확인한다.
-2. 연속 뷰 대신 익스텐트 열을 소비자에게 준다(HTTP `writev`, 피어 전송, TUI 렌더).
+1. 작업 중인 값을 앱이 따로 복사해 두지 않고 런타임이 소유한다 — [074](074-runtime-owned-drafts.md)에서 구현했다.
+2. 연속 뷰 대신 익스텐트 열을 소비자에게 준다(HTTP `writev`, 피어 전송, 렌더).
3. Pure 간 동기화에서 상대에게 없는 노드만 보낸다.
4. 코드 크기를 줄인다. 기본 저장소 승격 여부는 1–4의 결과와 macOS·전원 차단 검증 뒤에 판단한다.
diff --git a/docs/research/074-runtime-owned-drafts.md b/docs/research/074-runtime-owned-drafts.md
new file mode 100644
index 0000000..71ac8c9
--- /dev/null
+++ b/docs/research/074-runtime-owned-drafts.md
@@ -0,0 +1,97 @@
+# 074 — 작업 중인 값을 런타임이 소유한다
+
+2026-09-25. [073](073-single-file-persistent-tree.md)의 선택형 트리 저장소 위에 런타임 API
+[`state.h`](../../prototypes/connections/state.h)를 추가했다. 기본 빌드와 기본 저장 형식은 바뀌지 않았다.
+
+Pure는 JVM과 같은 위치의 런타임이다. 앱 기능은 별도 저장소에서 만들고, 이 저장소의 CLI·TUI·
+HTTP 호스트는 런타임을 검사하는 호스트다. 따라서 이번 단계는 편집기 기능이 아니라 모든 앱이 쓰는
+런타임 능력으로 만들었다.
+
+착수 기준:
+
+- 바꿀 결정: 앱이 작업 중인 값을 자기 메모리에 복사해 두고 저장할 때 저장본과 비교하는 관례를
+ 유지할지, 런타임이 작업 버전(draft)을 소유하고 앱은 연산만 보낼지.
+- 남은 질문: 초안이 저장본과 익스텐트를 공유하면 메모리가 실제로 줄어드는가? 긴 편집 세션에서
+ 경로 복사로 생긴 쓰레기가 쌓이지 않는가? 충돌·고장·압축에서도 계약이 유지되는가?
+- 최소 확인: 같은 저장소에서 "앱 복사 + 비교" 방식과 draft 방식을 같은 편집으로 비교하고,
+ 무작위 오라클·충돌·고장·파일 교체 검사를 통과시킨다.
+- 중단 기준: 메모리나 저장 시간이 줄지 않거나, 실패한 커밋이 draft나 저장본을 바꾸면 닫는다.
+
+## 의심한 관례
+
+| 관례 | 바꾼 것 |
+|---|---|
+| 앱이 값의 사본(버퍼·모델)을 가진다 | 런타임이 작업 버전을 소유한다. 바뀌지 않은 부분은 저장본의 불변 익스텐트를 그대로 가리킨다 |
+| 저장할 때 사본과 저장본을 비교해 바뀐 곳을 찾는다 | 편집 연산이 곧 변경이다. 커밋은 draft가 새로 만든 노드만 쓴다 |
+| 읽기 = 연속 버퍼로 복사 | 읽기 = 순서대로 빌려주는 익스텐트 열(무복사) |
+| "저장 안 된 편집"은 앱의 상태 | 커밋되지 않은 루트는 런타임 상태이며 커밋 전까지 파일에 쓰지 않는다 |
+
+## API
+
+- `pure_draft_open(path, key)`: 커밋된 버전에서 시작한다. 키가 없으면 revision 0의 빈 draft다.
+- `pure_draft_splice(offset, remove, insert)`: 메모리에서만 바뀐 leaf와 경로를 복사한다.
+- `pure_draft_read(offset, length, visit)`: 저장본 매핑 또는 draft 메모리의 익스텐트를 복사 없이 순서대로 빌려준다.
+- `pure_draft_commit()`: 키가 draft의 기준 revision이면 새 노드만 추가하고 공개한다. 성공하면
+ draft는 새 revision에서 이어가고, 실패하면 draft와 저장본 모두 그대로다.
+
+커밋 쓰기는 draft의 노드 메모리를 바꾸지 않는다. 그래서 동기화 전에 실패해도 draft를 다시 커밋할 수 있다.
+다른 키의 쓰기로 압축이 일어나 파일이 교체되면 draft의 저장 참조는 옛 파일을 가리킨다. 이때 커밋은
+draft가 가진 옛 매핑에서 필요한 노드를 새 파일로 복사한다(한 번 전체 쓰기).
+
+편집할 때마다 경로를 새로 만들므로 도달할 수 없는 노드가 생긴다. 이 쓰레기가
+`max(도달 가능한 바이트, 32 KiB)`를 넘으면 표시·회수한다.
+
+## 측정
+
+같은 Raspberry Pi 5, ext4 `/dev/mmcblk0p2`, 저장소는 둘 다 `store-tree.c`다. 1 MiB 문서 가운데에
+한 글자를 입력하고 저장하기를 24회 반복했고, 방식마다 3회 실행했다. 문서 생성은 별도
+프로세스에서 하여 편집 세션의 피크만 쟀다(Linux `VmHWM`).
+[측정 코드](../../research/experiments/009-runtime-drafts/bench.c),
+[원시 결과](../../research/experiments/009-runtime-drafts/2026-09-25-ext4-linux-aarch64.json).
+
+| 1 MiB 문서 | 앱 사본 + 비교 | 런타임 draft |
+|---|---:|---:|
+| 저장 중앙값 (실행별) | 10.08 · 10.53 · 13.57 ms | 9.25 · 8.64 · 9.13 ms |
+| 편집 세션 피크 RSS | 4,464–4,496 KiB | **1,648–1,664 KiB** |
+| 글자 하나 반영 | 0.04–0.06 µs (gap buffer) | 3.07–3.59 µs |
+
+주된 이득은 메모리다. draft는 문서 전체를 앱 메모리에 올리지 않고, 저장할 때 1 MiB를 다시
+복사하지도 않는다. 저장 시간은 약 1–2 ms 줄었을 뿐이다. 남은 대부분은 두 번의 `fsync`다.
+글자 하나 반영은 gap buffer보다 느리지만(leaf 하나와 경로 복사), 수 µs라 입력 한 번의 비용으로는 작다.
+
+## 검증
+
+[`test-tree-store.c`](../../prototypes/connections/test-tree-store.c) 추가 검사는 ASan·UBSan을 포함해 통과했다.
+
+- 20,000회 무작위 편집을 draft에서 평면 오라클과 비교하고 1,000회마다 커밋했다(최종 419,324 B, 커밋 20회).
+ 커밋 뒤에는 저장본 일치, revision 증가, 빈 arena를 확인했다.
+- 1 MiB 문서 가운데에서 30,000번 입력·삭제(순증 10,000 B): arena 피크 47,339 B.
+ 커밋은 14,952 B만 추가했다.
+- 두 draft가 같은 키를 수정하면 두 번째 커밋은 충돌을 내고 자기 편집을 유지한다.
+- 다른 키의 쓰기로 파일이 압축·교체된 뒤에도 draft 커밋이 올바른 내용을 공개한다.
+- 다섯 고장 단계: 동기화 전 실패는 저장본·파일 크기·draft를 바꾸지 않는다. 결과 불명 뒤의 재커밋은
+ 충돌로 드러나고, 다시 열면 공개된 내용을 본다.
+- 범위 밖 편집·읽기는 draft를 바꾸지 않는다. 읽기는 방문자가 멈추면 중단된다.
+
+`make check-tree` 전체(기존 런타임·부분 수정·비동기·실제 PTY 검사 포함)도 다시 통과했다.
+
+## 비용과 한계
+
+- `store-tree.c` 오브젝트 `text`가 15,899 B에서 19,321 B로 늘었다. draft를 쓰지 않는 트리 CLI도 같은
+ 번역 단위라 함께 링크된다(34,913 B). 분리하거나 줄여야 한다.
+- 아직 런타임 코어(`runtime.c`)의 작업·예산·완료 모델이나 `.pure` 정의에서 draft를 다룰 수 없다.
+ 지금은 호스트 수준 C API다. draft 메모리는 런타임 작업 예산에 계상되지 않는다.
+- 기존 TUI는 그대로 자기 gap buffer와 비교 방식을 쓴다. 앱 쪽 전환은 앱 저장소의 일이다.
+- 커밋은 기준 revision이 다르면 충돌로 끝난다. 두 draft의 연산을 자동으로 합치지 않는다.
+- draft는 기준 파일의 매핑을 끝까지 유지한다. 압축으로 교체된 옛 파일의 공간은 draft를 닫을 때까지
+ 회수되지 않는다. 최신 head가 전원 차단 등으로 손상돼 이전 커밋으로 되돌아가면, 그 뒤를 매핑한 draft의
+ 읽기는 잘린 파일 영역에 닿을 수 있다(기존 저장소의 외부 절단과 같은 신뢰 경계).
+- 한 draft는 한 스레드가 소유한다. 물리 전원 차단, macOS, x86_64는 검증하지 않았다.
+
+## 다음 결정
+
+1. draft를 런타임 코어의 자원으로 만든다. 작업 예산에 계상하고, `.pure` 정의가 draft 연산과
+ 커밋을 효과로 선언하게 한다. 앱은 C API가 아니라 런타임 계약으로 작업 버전을 다룬다.
+2. 호스트에 익스텐트 열을 그대로 넘긴다(HTTP `writev`, 피어 전송). 연속 복사는 호환 경계에만 남긴다.
+3. 같은 키의 동시 draft에서 서로 겹치지 않는 편집을 합칠지(연산 재기준화) 판단한다.
+4. 코드 크기를 줄이고 draft를 쓰지 않는 호스트에서 빠지게 한다.