diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/008-module-format.md | 15 | ||||
| -rw-r--r-- | docs/research/082-json-in-place.md | 78 |
2 files changed, 88 insertions, 5 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 0f0a6d8..5156e0a 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -266,16 +266,21 @@ text·리스트가 남을 때만 위치를 유지한다. |---|---| | `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) | | `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` | -| `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수)를 쿼리(`?points=3`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수). 응답 본문은 핸들러의 `reply`(메시지 필드) | +| `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수)를 쿼리(`?points=3`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) | | `GET`·`PUT /objects/키` | 모듈에 키별 메시지 `show`·`edit`가 있으면 기존 문서 서버(`server.c`)와 같은 계약. `GET`은 `show`, `PUT`(`If-Match`/`If-None-Match`)은 `edit` | - **실패는 실패 종류로 답한다.** 이벤트가 실패하면 `X-Pure-Error`에 종류를 싣는다. `not_found` 404, `exists` 409, `limit`·`divide`·`clamp`·`non_finite`·`encoding` 422, UTF-8이 아닌 text 400. 값이 없는 보기는 404 `no_value`, 없는 이름은 404 `no_view`·`no_input`, 모자라거나 모르는 쿼리 필드는 400 `missing_field`·`query`다. -- **JSON은 연결 스레드가 풀어 둔다.** 앱 입력의 ingress는 `[u32 페이로드 크기][페이로드][온 그대로의 JSON]`이다. 본문을 - 다 받으면 연결 스레드가 JSON을 메시지의 페이로드로 옮기고(이스케이프, 서로게이트 쌍, UTF-8, 수의 문법과 범위를 확인한다) - 나서야 요청이 준비된다. 그래서 앱 소유자는 해석하지 않고, 응답은 여전히 요청에서 빌린다. 필드가 빠지거나(`missing_field`), - 모르거나 두 번 오거나(`json_field`), 타입이 다르거나(`json_type`), JSON이 아니면(`json`) 400이고 요청은 실행되지 않는다. +- **JSON은 연결 스레드가 제자리에서 읽는다.** JSON 본문을 가진 앱 입력의 ingress는 `[u32 페이로드 크기 | 2^31][필드마다 + u32 시작과 크기][페이로드: 접두부, 그다음 온 그대로의 JSON]`이다. 본문을 다 받으면 연결 스레드가 JSON을 한 번 훑는다(8바이트씩). + 수는 접두부에 넣고, 문자열은 JSON 안의 그 자리를 표에 적는다. 이스케이프가 있는 문자열만 그 자리에 풀어 쓴다(언제나 + 짧아진다). 이스케이프, 서로게이트 쌍, UTF-8, 수의 문법과 범위를 확인한 뒤에야 요청이 준비되고, 소유자는 표대로 메시지를 + 적용한다(`pure_app_message_fields`: 가변 필드가 페이로드 어디에 있든 자리를 받는다). 그래서 앱 소유자는 해석하지 않고, + ingress는 본문 크기에 표만 더하며, 응답은 여전히 요청에서 빌린다. 필드가 빠지거나(`missing_field`), 모르거나 두 번 오거나 + 쿼리와 본문에 함께 오거나(`json_field`), 타입이 다르거나(필드 자리에 그룹, 그룹 자리에 값, 배열, `true`·`false`·`null`: + `json_type`), JSON이 아니면(`json`) 400이고 요청은 실행되지 않는다. 256 KiB 본문의 요청이 CPU 0.25 ms다 + ([082](../research/082-json-in-place.md)). - **라우트는 작업 슬롯에 둔다.** 요청이 가리키는 것(문서, 보기, 입력)과 메시지 필드는 받을 때 풀어 슬롯별 표와 ingress에 둔다. 연결이 묶음 전에 끊겨도 받아들인 이벤트는 실행된다. 런타임 코어는 그대로다(키는 문서 키 또는 보기·입력 이름). - **시간.** 요청의 이벤트는 적용되는 순간의 벽시계를 `now`로 본다. 앱을 돌리는 스레드(한 스레드면 그 스레드, 아니면 diff --git a/docs/research/082-json-in-place.md b/docs/research/082-json-in-place.md new file mode 100644 index 0000000..6a456b8 --- /dev/null +++ b/docs/research/082-json-in-place.md @@ -0,0 +1,78 @@ +# 082 — JSON 본문은 제자리에서: 그룹 필드, 복사 없는 문자열, 8바이트씩 훑기 + +2026-09-27. [로드맵](../roadmap.md) 단기 4 "JSON 본문 역직렬화 확장". + +착수 기준: + +- 바꿀 결정: 앱 입력의 JSON 본문을 메시지 페이로드로 옮겨 적을지(문자열을 풀어 복사), 본문을 그 자리에 두고 필드의 + 위치만 넘길지. 중첩된 JSON 객체를 받으려면 메시지 형식에 무엇이 필요한지. +- 남은 질문: 여러 text 필드는 이미 받는다([077](077-apps-as-http-services.md)). 그러나 문자열을 모두 페이로드로 복사해 + ingress가 본문의 두 배였고, 중첩 객체는 받지 못했다. 복사를 없애면 무엇이 달라지는가? 중첩을 실행 비용 없이 표현할 수 + 있는가? +- 최소 확인: 같은 요청을 복사 빌드와 새 빌드로 번갈아 재고(처리량, 요청당 CPU, 최대 RSS), 무작위 JSON(이스케이프, 제어 + 바이트, 서로게이트 쌍, 한글, 이모지, 섞인 멤버 순서, 그룹)을 파이썬 모델과 대조한다. +- 중단 기준: 모델과 한 번이라도 다르거나 틀린 요청 하나라도 받아들이면 되돌린다. + +## 의심한 관례 + +| 관례 | 이 설계 | +|---|---| +| 요청 본문은 파싱해서 객체 트리로 만든 뒤 필드를 꺼낸다 | 본문을 한 번 훑으며 필드의 자리(시작, 크기)만 적는다. 트리도 사본도 없다 | +| 문자열은 풀어서(unescape) 새 버퍼에 담는다 | 이스케이프가 없는 문자열은 본문의 그 바이트가 필드다. 있는 것만 그 자리에 풀어 쓴다(언제나 짧아진다) | +| 중첩 객체는 중첩 타입(클래스, 구조체)으로 받는다 | 그룹은 이름의 경로다(`to.city`). 모듈에서는 평평한 필드라 런타임은 아무것도 더 하지 않는다 | + +## 바꾼 것 + +- **그룹.** aup 메시지의 필드 블록 안에 `to: { city: text, zip: u32 }`를 쓸 수 있고(겹쳐도 된다), 핸들러는 + `ship.to.city`로 읽는다. 컴파일러는 그룹을 `to.city` 같은 경로 이름의 필드로 펼친다. Pure 모듈 형식은 메시지 필드 + 이름에 식별자를 점으로 이은 경로를 받고, 한 이름이 필드이면서 그룹인 모듈(`user`와 `user.name`)은 거절한다. 모듈의 + 메시지 필드 한도는 32에서 64로 올렸다(`PureModule` 11.9 → 13.2 KiB). +- **자리를 받는 메시지.** `pure_app_message_fields`는 가변 필드마다 페이로드 안의 시작과 크기를 표로 받는다. 필드가 + 접두부 뒤에 차례로 있을 필요가 없다. 런타임은 전처럼 범위와 UTF-8을 확인한다. +- **제자리 JSON.** `pure-serve`의 JSON 입력 ingress는 `[크기 | 2^31][필드마다 시작·크기][접두부][온 그대로의 JSON]`이다 + (전에는 `[크기][접두부 + 풀어 쓴 문자열 자리(본문 크기)][JSON]`). 연결 스레드는 본문을 한 번 훑어 수는 접두부에 넣고, + 문자열은 자리를 적는다. 그룹은 안의 객체로 받고, 쿼리의 고정 필드는 경로(`?to.zip=7`)로 받는다. 필드 자리에 그룹, + 그룹 자리에 값, 배열, `true`·`false`·`null`은 `json_type`, 모르는 멤버(그룹 안 포함)는 `json_field`다. +- **8바이트씩.** 문자열의 끝을 찾을 때 따옴표·백슬래시·제어 바이트가 없는 8바이트는 한 번에 넘긴다. UTF-8 검사도 글자 + 사이의 ASCII 8바이트를 한 번에 넘긴다(`pure_utf8_step`, 모든 text 검사가 함께 빨라진다). + +## 장치가 말한 것 + +Raspberry Pi 5, 서버는 CPU 0–2(연결 스레드 하나), 부하는 CPU 3. 상태 없는 앱 `echo` +([echo.aup](../../benchmarks/http/echo.aup): text 필드 둘, 이름을 답한다)에 `{"name":"k","text":"x…"}`를 4초간 +보낸다(`load … json`). 세 빌드를 번갈아 두 번씩 쟀다. + +| 본문 | 복사 | 제자리 | 제자리 + 8바이트씩 | +|---|---:|---:|---:| +| 256 KiB, 8연결: 요청/s | 652–655 | 823–826 | **3,737–3,953** | +| 요청당 서버 CPU | 1,517–1,529 µs | 1,208–1,212 µs | **252–262 µs** | +| 최대 RSS | 5.2 MiB | 3.2–3.6 MiB | **3.2–3.4 MiB** | +| 4 KiB, 64연결: 요청/s | 29,139–29,150 | 33,548–34,609 | **76,270–76,695** | +| 요청당 서버 CPU | 34.2 µs | 28.8–29.7 µs | **13.0–13.1 µs** | +| 64 B, 64연결: 요청/s | 93,183–93,246 | 92,766–92,929 | — | + +- 복사를 없애자 큰 본문의 메모리가 3분의 1 줄고 요청당 CPU가 20% 줄었다. 작은 본문은 같다. +- 남은 CPU의 대부분은 바이트마다 도는 두 루프였다. 문자열의 끝 찾기와 UTF-8 검사(연결 스레드에서 한 번, 런타임에서 한 + 번)다. 8바이트씩 넘기자 256 KiB 요청이 1.2 ms에서 0.25 ms가 됐다. 이제 초당 약 1 GB를 훑는다. + +## 검증 + +- `test-module`: 가변 필드를 JSON 본문 안의 자리로 받아(순서가 선언과 달라도) 전과 같은 상태와 답이 된다. 페이로드 밖에서 + 시작하거나 끝나는 자리는 `limit`, UTF-8이 아닌 자리는 `encoding`으로 실패하고 아무것도 바꾸지 않는다. 경로 이름 + `user.name`은 받고, `user..name`, `.user`, `user.`, `user.1st`, `user name`, 그리고 `user`와 `user.name`을 함께 가진 + 모듈은 거절한다. +- aup `make check`의 `server_json`: 전의 무작위 평평한 JSON 300개와 틀린 요청 14가지에 더해, 그룹이 두 단계인 메시지에 + 무작위 JSON 100개(멤버 순서를 단계마다 섞고, 문자열에 이스케이프·제어 바이트·한글·이모지·U+2028)를 보내 답과 표가 + 파이썬 모델과 같다. 그룹의 고정 필드를 쿼리 경로로 준 요청, 틀린 그룹 요청 10가지(모르는 멤버, 그룹 자리에 값, 필드 + 자리에 객체, 빈 그룹, 두 번 온 필드, 배열, `null`, 외톨이 서로게이트, 닫히지 않은 객체), 쿼리와 본문에 함께 준 필드도 + 확인한다(모두 424건). 진단 6가지(그룹을 값으로 읽기, 그룹에 없는 필드, 필드와 그룹이 같은 이름, 그룹 두 번, 빈 그룹, + 31자를 넘는 경로)를 더했다. +- 나머지 aup·Vega 검사, Pure `make check`·`check-tree`가 통과하고, ASan·UBSan 빌드의 `pure-serve`로 서버 검사 + (모델 대조, 대여, JSON, 타이머)를 통과한다. + +## 다음 결정 + +- 배열(리스트 필드)은 아직 받지 않는다. 받으려면 메시지에 반복되는 그룹이 있어야 하고, 핸들러가 그것을 도는 방법(리스트 + 상태에 한꺼번에 더하기 같은)이 먼저 정해져야 한다. +- UTF-8을 연결 스레드와 런타임이 두 번 검사한다. 런타임의 검사는 호스트를 믿지 않기 위한 경계라 남긴다. 연결 스레드의 + 검사는 400 `json`으로 답하기 위한 것이다. 둘을 합치려면 런타임의 실패를 요청 오류로 옮겨 답하는 길이 필요하다. |
