# 메모리와 데이터 수명 초안 0.1 상태: **제안, 실행기 미구현**. 현재 Workbench에는 바이트 버퍼·비동기 완료 큐가 없다. 이 문서는 [연산 의미](001-operation-semantics.md)를 구현할 때의 불변조건과 회수 규칙을 정한다. ## 1. 예산의 소유자 각 실행 영역에 바이트 예산과 객체·요청 개수 예산을 부여한다. 영역의 소유자는 런타임이다. 첫 구현 후보는 고정 크기 청크 풀과 세대 번호를 가진 핸들 테이블이다. 장기 상태·스트림 문맥·작업 임시 공간은 수명이 달라 별도 항목으로 센다. 이 정책이 최적이라고 가정하지 않으며 일반 할당 방식과 같은 작업으로 비교한다. | 계측할 공간 | 반드시 포함할 것 | |---|---| | 확보한 backing 용량 | 사용 중인 청크와 비어 있지만 보유 중인 청크, 정렬·단편화 | | 메타데이터 | 핸들·대여·문맥·작업·완료·대기열 슬롯 | | 살아 있는 데이터 | 앱·소비자·진행 중 작업이 여전히 참조하는 실제 버퍼 | | 모듈 내부 공간 | 코덱 윈도·사전·파싱 상태·작업 공간·라이브러리의 자체 할당 | | 교체 중 공간 | 이전 코드·문맥·진행 중 작업과 다음 프로그램의 준비 공간 | | 외부 자원 | 별도 호스트 힙·OS·GPU 자원과 추적할 수 없는 할당 | 같은 물리 버퍼를 여러 독자가 대여해도 물리 용량은 한 번만 센다. 물리 용량과 사용량은 포함 관계이므로 합산하지 않는다. 모듈이 예산 밖에서 할당하면 그 공간을 보고서에서 제외하지 않고 외부·미계측으로 표시하며 전체 상한 보장을 중단한다. 요청별 예산을 만들었다고 라이브러리의 숨은 할당이 자동 통제되는 것은 아니다. 풀에서 할당한 모듈 작업 공간·메타데이터는 그 풀 확보 용량의 세부 귀속 항목이다. 풀 용량에 다시 더하지 않는다. 확보 용량은 할당한 backing의 크기이며 OS의 상주 RAM 측정값과 다르다. 현재 64 KiB는 Wasm 선형 메모리의 초기·최대 용량이다. JS 호스트·컴파일러·렌더러까지 포함한 전체 예산이 아니다. 새 버퍼 풀을 그 공간 밖에 만들면 별도로 포함해야 한다. ## 2. 버퍼 식별과 범위 `BufferHandle = (slot:u32, generation:u32)`를 첫 후보로 정한다. 포인터·파일 오프셋이 아니다. 세대 0은 무효, 처음 발급은 1이다. 슬롯 재사용마다 세대를 증가시키고 최댓값에 도달한 슬롯은 회수 후 영구 퇴역한다. 순환으로 오래된 핸들이 다시 유효해지지 않게 한다. 핸들은 발급한 실행 영역 안에서만 의미가 있다. 다른 영역으로 숫자만 복사해 전달할 수 없다. 구간은 핸들과 `offset:u32, length:u32`다. 런타임은 다음을 검사한다. - 현재 세대·소유자·요청자 권한·접근 모드가 일치한다. - 읽기는 현재 유효 데이터 길이, 쓰기는 예약 용량 안에 있다. - `offset <= bound`를 먼저 확인하고 `length <= bound - offset`을 검사해 덧셈 오버플로를 피한다. - 정렬 요구를 충족한다. v0 바이트 프로파일의 기본 정렬은 1이며 더 큰 정렬은 모듈 설명에 명시한다. - v0 출력 버퍼는 현재 유효 길이 0, 출력 구간 오프셋 0이어야 한다. 접수 때 암묵적으로 이전 데이터를 버리지 않는다. 소비가 끝난 뒤 대여가 없는 소유자가 명시적으로 유효 길이를 0으로 줄여 재사용한다. 유효 길이를 늘리는 일은 검증된 쓰기 완료·유효 출력 공개로만 허용한다. 같은 프로세스의 임의 네이티브 코드로부터 메모리를 격리하는 기능은 아니다. 이 계약의 첫 대상은 계약을 지키는 내부 모듈이다. 불신 모듈의 격리는 별도 실행 경계가 필요하다. ## 3. 소유권과 대여 한 버퍼에는 소유자 하나가 있으며 런타임이 대여 기록을 관리한다. 복사 없는 전달의 후보는 버퍼 내용 대신 이 소유권 또는 읽기 대여를 넘기는 것이다. | 상태 | 허용하는 일 | 금지하는 일 | |---|---|---| | 소유, 대여 없음 | 읽기·독점 쓰기·소유권 이전·반납 | 범위 밖 접근 | | 읽기 대여 1개 이상 | 대여받은 독자의 읽기, 추가 읽기 대여 | 쓰기·소유권 이전·재사용·반납 | | 독점 쓰기 대여 1개 | 지정된 작성자의 범위 내 쓰기 | 다른 읽기·쓰기·이전·재사용·반납 | | 반납 완료 | 같은 슬롯의 새로운 세대로 재할당 가능 | 이전 핸들 접근 | 첫 구현은 **서로 다른 구간이라도 버퍼 전체 단위로 대여**한다. 구간별 동시 쓰기나 동적 겹침 증명은 후속 연구다. 읽기·쓰기 대여가 동시에 존재하지 않는다는 조건을 단순하게 유지한다. 모듈 호출을 위해 해석한 실제 포인터는 허용된 접근 기간에만 유효하다. 동기 백엔드는 함수 반환 후 포인터를 사용하지 않는다. 비동기 백엔드는 최종 완료 게시 전에 접근을 끝낸다. 런타임에 접수된 요청의 대여는 백엔드의 동기·비동기 여부와 관계없이 **완료 회수까지 유지**한다. 모듈이 계속 데이터를 보유해야 한다면 자체 문맥에 예산을 잡아 복사하거나, 별도의 추적 가능한 장기 대여가 필요하다. 첫 프로파일은 호출 종료 뒤 입력·출력 포인터를 숨겨 보유하는 것을 금지한다. `반납`은 대여가 있으면 `BUSY`로 실패하고 소유권을 유지한다. 암묵적 지연 반납을 만들지 않는다. 비밀 데이터 버퍼의 지우기는 마지막 접근이 끝난 뒤 수행하며 사용한 라이브러리의 보장·한계와 복사본의 수명까지 별도로 확인한다. 단순히 슬롯 번호를 지우는 것으로 데이터 삭제를 주장하지 않는다. ## 4. 비동기 요청의 수명 첫 계약은 한 실행 영역의 직렬 제어 흐름을 사용한다. 모듈의 worker는 완료를 게시하고 상태·핸들 테이블의 회수는 영역 소유자가 수행한다. 제어 API의 재진입을 허용하지 않는다. 스트림 문맥 하나에는 진행 중 호출을 최대 하나만 허용한다. ```text 준비 → 런타임 접수 거절 : 예약 해제, 티켓 없음 준비 → 런타임 접수/확정 → 백엔드 제출 → 완료 게시 → 완료 회수 → 취소 요청 → 완료 게시 → 완료 회수 ``` - 접수 전 요청 슬롯·대여 슬롯·완료 보관 슬롯을 함께 확보한다. 실패하면 `WOULD_BLOCK` 또는 `LIMIT`이다. - 런타임 접수 전 거절은 입력·출력·문맥을 바꾸지 않는다. 접수된 요청에는 세대가 붙은 티켓 하나를 발급한다. 이벤트 안의 접수는 준비된 관리 상태와 함께 확정한다. 실제 백엔드 제출은 그 이후다. 백엔드 제출 거절은 기존 티켓의 실패 완료로 보고하며 티켓 없는 거절로 되돌리지 않는다. - 모든 접수된 요청은 성공·실패·취소 중 하나의 최종 완료로 정리해야 한다. 실제 백엔드가 응답하지 않을 수 있으므로 유한 시간 안에 완료된다는 보장은 별도다. - 즉시 완료해도 제출 호출 안에서 앱 콜백을 재진입하지 않고 완료 큐로 전달한다. - 취소 호출은 **요청**이다. 거절되거나 정상 완료와 경합할 수 있다. 버퍼를 반납할 권한을 주지 않는다. - 완료 게시 뒤에도 앱이 완료를 회수하기 전에는 해당 요청의 대여·슬롯을 보유한다. - 완료 회수는 티켓·모듈 세대·길이·상태를 검증하고 유효 출력 길이와 문맥 상태를 반영한 뒤 대여·요청 슬롯을 한 번만 해제한다. 중복·오래된 완료는 진단하고 두 번 반납하지 않는다. v0은 출력 버퍼의 유효 길이를 해당 요청의 `produced`로 설정한다. 실패·취소도 동일하며 물리적으로 썼지만 검증되지 않은 꼬리 부분은 공개하지 않는다. - 중복 완료를 무시하는 것만으로 잘못된 백엔드의 추가 메모리 쓰기가 안전해지는 것은 아니다. 모듈은 완료 게시 전에 작업·worker·장치의 접근 종료와 필요한 동기화를 보장해야 한다. - 제한 시간 초과만으로 메모리를 재사용하지 않는다. 실제 접근이 종료됐다는 백엔드의 보장까지 보유한다. libuv도 취소 요청 후 완료 콜백 전에는 요청 관련 메모리를 해제하면 안 된다고 명시한다. 이 원칙을 Pure의 대여·티켓 상태에 반영했다. 취소에 외부 효과의 롤백을 포함하지 않는다. [libuv 요청·취소 계약](https://docs.libuv.org/en/v1.x/request.html#c.uv_cancel) ## 5. 포화·느린 소비자·교체 요청·완료·버퍼 슬롯이 없으면 생산자에게 역압력을 반환한다. 완료를 조용히 버리거나 대기 요청을 무제한 늘리지 않는다. 앱은 재시도 시점·거절·명시적 버리기 정책을 선택한다. 대여를 오래 유지하는 소비자는 메모리를 붙잡는다. 복사량뿐 아니라 유지 용량과 바이트×시간을 센다. 소유권 전달을 했다는 사실만으로 GPU·드라이버·프로세스 경계의 복사가 없어졌다고 주장하지 않는다. 프로그램·모듈 교체의 첫 정책은 **관련 진행 작업·대여·열린 문맥이 있으면 `BUSY`로 거절**하는 것이다. 작업을 배출하고 명시적으로 문맥을 닫은 뒤 교체한다. 시간 초과로 강제 언로드하지 않는다. 다중 버전을 동시에 유지하는 무중단 교체는 첫 계약 밖이다. 현재 이름 기반 숫자 상태 보존은 파일 핸들·코덱 문맥·키·GPU 자원의 자동 이전을 뜻하지 않는다. ## 6. 구현 시 필수 확인 아직 통과한 검사가 아니라 다음 구현의 합격 조건이다. 1. 버퍼 2개와 요청 1개로 여러 번 제출·완료·반납해 슬롯 수와 예산이 보존된다. 2. 포화된 제출은 상태·유효 길이·소유권을 변경하지 않는다. 3. 취소 요청만 받은 출력 버퍼는 다른 읽기·쓰기·반납이 거절된다. 입력 버퍼의 공유 읽기는 가능하나 쓰기·반납은 거절된다. 관련 문맥·모듈 교체도 거절되고 최종 완료 회수 전에는 해제하지 않는다. 4. 늦은 완료·중복 완료·해제된 세대의 핸들이 새 버퍼를 회수하거나 수정하지 않는다. 5. 실패 출력은 명시한 유효 접두부만 공개한다. 인증 실패 단위에서는 유효 길이가 0이다. 6. 입력 소비 완료 후 남는 출력·종료 처리가 정확히 진행된다. 느린 소비자에서 사용량이 예산을 넘지 않는다. 7. 길이·누적 크기 넘침, 세대 소진, 할당 실패를 명시적으로 처리한다. 8. 모듈 내부 공간과 대기 작업까지 포함한 피크를 계산하고 외부 할당과 전체 RSS를 따로 보고한다.