diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/research/073-single-file-persistent-tree.md | 10 | ||||
| -rw-r--r-- | docs/research/074-runtime-owned-drafts.md | 97 |
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를 쓰지 않는 호스트에서 빠지게 한다. |
