1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
# 074 — 작업 중인 값을 런타임이 소유한다
2026-09-25. [073](073-single-file-persistent-tree.md)의 선택형 트리 저장소 위에 런타임 API
[`state.h`](../../core/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`](../../store/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를 쓰지 않는 호스트에서 빠지게 한다.
|