aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/074-runtime-owned-drafts.md
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 09:34:34 +0100
committereunhhu <[email protected]>2026-09-25 09:34:34 +0100
commit65386f68fb53e5a9a20a284b06afd872172c30eb (patch)
treeb04133aaa602fbf6a864df1d368ae091ab09b600 /docs/research/074-runtime-owned-drafts.md
parent6a28879bd56354483fe940c0c80d511eccb89983 (diff)
downloadpure-65386f68fb53e5a9a20a284b06afd872172c30eb.tar.gz
pure-65386f68fb53e5a9a20a284b06afd872172c30eb.zip
Let the runtime own working versions as drafts
state.h gives any app a runtime-owned working version of a stored value on the tree store. A draft shares unchanged extents with the stored version, edits in memory by path copying, lends ordered extents without copying, and commits only its new nodes when the key is still at its base revision. A failed commit changes neither the draft nor the store; a draft whose file was replaced by compaction copies its stored extents on commit. Unreachable arena nodes are reclaimed once they outweigh max(reachable, 32 KiB). Typing into the middle of a 1 MiB document and saving each character peaked at about 1,660 KiB RSS with a draft against about 4,480 KiB for an app copy compared with storage on save; saves were only 1-2 ms faster. This corrects 073, which had attributed the TUI's remaining 8 ms to that comparison. Drafts are not yet charged to the runtime budget or visible to .pure definitions.
Diffstat (limited to 'docs/research/074-runtime-owned-drafts.md')
-rw-r--r--docs/research/074-runtime-owned-drafts.md97
1 files changed, 97 insertions, 0 deletions
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를 쓰지 않는 호스트에서 빠지게 한다.