상주 VM 세션 프로토타입 — 키1타 90~290ms 근본 처방 (P1)
개요
CrownyBrowser 네이티브 터미널 앱(crowny://app/터미널)의 키 입력 1타 왕복이
90~290ms 걸리는 병목의 뿌리는 crownyc 콜드 프로세스 스폰 3~4회
(입력코어 → 터미널앱(cmd IR) → python 페이지배선.py → 페이지렌더러) + 매
스폰마다 바이트코드(3595~6409큐브) 재로드다. 선례(brainq :9977, 게이트웨이
8080)와 같은 패턴으로 입력·렌더 로직을 TCP 상주 프로세스로 병합하는
프로토타입을 만들어 실측했다.
무엇을 했는지
src/터미널상주서버.한선신규 작성 — 순수 한선씨, TCP대기/TCP수락
src/입력코어.한선 — 키/캐럿/선택 전체 디스패치(FOCUS/KEY/BS/LEFT/
RIGHT/HOME/END/SELALL/PASTE/COPY/CUT/CLICKCARET) 그대로 이식,
출력값() 대신 문자열 반환으로 변경.
- src/터미널앱.한선 — cmd IR 렌더 로직(헤더/스크롤백/입력칸/버튼) 이식.
단, 렌더 프리미티브는 src/화면.한선의 _발행()=출력값() 고정 배선을
그대로 쓰면 상주 프로세스의 stdout(서버 로그용으로 남겨둠)과 충돌하므로
_r 접두 로컬 버전으로 재구현 — 덧쓰기()로 /tmp/터미널.cmd에 직접
기록. verb 포맷(INIT/RECT/ROUND/SHADOW/TEXT/END)은 화면.한선과 동일해
기존 페이지배선.py 파이프라인과 바이트 호환.
- 화면.한선/터미널앱.한선/입력코어.한선/crowny-browser.m/기존
렌더sh는 일절 수정하지 않음 (가드레일 스코프 준수, 다른 세션 병렬
작업 보호).- 프로토콜 (TCP, 1줄=1요청,
\n종결):
키|<모드>|<값> 예) 키|KEY|a, 키|FOCUS|터미널입력, 키|BS
→ /tmp/입력상태.psv 갱신 → OK|<값>|<캐럿> (COPY/CUT은 SEL|<텍스트>,
실패는 오류:...)
- 렌더 또는 렌더|CW|CH → /tmp/터미널상태.psv 읽어 cmd IR 생성 →
/tmp/터미널.cmd 기록 → OK|cmd
- 종료 → OK|종료 응답 후 accept 루프 종료(프로세스 자체 종료)- 상태 일관성:
/tmp/입력상태.psv,/tmp/터미널상태.psv는 파일이
crownyc run 입력코어.toau / 키입력처리.toau)와 상주
경로를 번갈아 호출해도 같은 파일을 공유하므로 안전(혼용 가능) — 실측
확인(스모크 테스트에서 FOCUS→KEY x3→BS 순서로 상태가 정확히 누적/갱신됨).- 벤치마크 (부팅 후 워밍업 1회, N=100/20,
bash /dev/tcp로 서브프로세스
nc 를 매 요청 재fork하는 방식으로 측정했더니
nc 자체의 프로세스 생성비용이 crownyc 스폰비용과 비슷해 시그널이
묻혔음. 순수 서버측 지연만 격리하려면 클라이언트도 무스폰이어야 한다는
것 자체가 이번 프로토타입의 부수적 발견). | 구간 | 경로 | p50 | p95 | min | max | N |
|---|---|---|---|---|---|---|
| 키 입력(KEY\|a) | 콜드 스폰(crownyc run 입력코어.toau) | 12ms | 12~13ms | 10~11ms | 14~21ms | 100 |
| 키 입력(KEY\|a) | 상주 세션(TCP 왕복) | 4ms | 5ms | 3ms | 5ms | 100 |
| cmd 렌더 | 콜드 스폰(crownyc run 터미널앱.toau, CROWNY_OUT=cmd) | 20ms | — | 19ms | 20ms | 20 |
| cmd 렌더 | 상주 세션(TCP 왕복) | 8ms | — | 8ms | 13ms | 20 |
두 번 재현(스폰 12ms/상주 4ms 안정적 재현). 목표 p50 <20ms 달성 (상주 키 왕복 4ms, 렌더 왕복 8ms — 둘 다 여유 있게 하회). 두 구간을 합치면 스폰 경로 32ms → 상주 경로 12ms, 약 2.7배 — 실제 사용자 체감 파이프라인은 여기에 python(페이지배선.py) 1회 스폰 + 페이지렌더러(전면 PPM) 스폰이 더 붙으므로, 이 두 단계만 병합해도 스폰 2/4회 제거 + 해당 구간 지연 ~1/3로 축소.
환경 참고: 이 macOS 개발기에는 crownyc 상주 서비스 100여 개가 이미
떠 있어(브라우저/게이트웨이/월렛/체인 등, crowny-sysguard 관리 대상
정상 멀티인스턴스) 스케줄링 경합이 있다 — 절대치는 유동적이나
스폰 vs 상주의 상대적 배율(약 2.5~3배) 은 두 차례 실측에서 일관됨.
관련 파일
/Users/ef/CrownyBrowser/src/터미널상주서버.한선— 신규 프로토타입 서버(순수 한선씨)/Users/ef/CrownyBrowser/src/입력코어.한선— 이식 원본(키/캐럿/선택), 미수정/Users/ef/CrownyBrowser/src/터미널앱.한선— 이식 원본(cmd 렌더), 미수정/Users/ef/CrownyBrowser/src/화면.한선— 렌더 프리미티브 원본(참조만, 미수정)/Users/ef/CrownyBrowser/native/crowny-browser.m— 현재 spawn 배선(679~688행
appRunInputCoreCmd, 앱목록.psv 터미널렌더.sh 참조) — 미수정
/Users/ef/CrownyBrowser/native/터미널렌더.sh— 현재 렌더 파이프라인(spawn
- 벤치 하네스(임시,
/tmp스크래치패드):상주벤치_smoke.sh,
상주벤치_비교2.sh, 렌더벤치.sh
(/private/tmp/claude-501/-Users-ef/929a8ec9-1c47-4929-9008-6af6618baaf0/scratchpad/)
- 학습:
crownycode-learn.sh add 터미널상주서버_TCP상주세션_키캐럿선택_cmdIR등록됨
브라우저 배선 계획 (다음 단계 — 이번 세션은 문서만, 코드 미적용)
- 서버 수명주기: LaunchAgent 아님.
crowny-browser.m의 앱탭 생성 시점
CrownyAppView 가 appService == "터미널" 인스턴스를 만들 때) 에
CROWNY_TERMSRV_PORT 를 하나 발급(crowny-ports.sh free 로 확보한
고정 포트 또는 탭별 동적 포트)해 NSTask 로 `crownyc run
/tmp/터미널상주서버.toau` 를 비동기(waitUntilExit 안 함) 기동하고
PID를 탭 객체(CrownyTab)에 보관. 탭이 닫히거나 앱이 종료될 때
kill(pid, SIGTERM) (기존 appRunTask 동기 NSTask 패턴과 별개로,
상주 프로세스는 탭 수명과 결합된 장수명 NSTask 핸들 필요).
- 키 입력 배선:
appRunInputCoreCmd(mode, value)(679~688행)를
appRunInputCoreCmdResident(mode, value) 를 신설 — NSTask 스폰
대신 connect()+write()+read() 로 127.0.0.1:<포트> 에
"키|<mode>|<value>\n" 전송, 응답 파싱(OK|값|캐럿 / SEL|... /
오류:...). 소켓 연결 실패(서버 미기동/죽음)시 기존 appRunInputCoreCmd
spawn 경로로 폴백(아래 실패 모드 참고) — 두 경로 모두 같은
/tmp/입력상태.psv 를 SSOT로 쓰므로 세션 중간에 경로가 바뀌어도 상태
불일치 없음(이번 프로토타입에서 검증한 속성).
- 렌더 배선:
터미널렌더.sh의 4단계 중 1~2단계(cmd IR 생성)만
"렌더|<CW>|<CH>" 요청으로 대체. python 페이지배선.py +
페이지렌더러(PPM) 단계는 이번 프로토타입 범위 밖 — 터미널렌더.sh 는
그대로 두되, "터미널앱.toau 실행" 스텝만 조건부로 상주서버 TCP 호출로
바꾸는 옵션 추가(CROWNY_TERMSRV_PORT 환경변수가 설정되어 있고 응답이
오면 사용, 아니면 기존 crownyc run /tmp/터미널앱.toau 그대로).
- 다음 프로토타입 범위: 페이지렌더러(cmd→PPM)까지 상주화하려면
화면시작()이 전체를 다시 그림), PPM 파일 쓰기도 상주 프로세스가
담당하도록 확장 — 이번 P1 프로토타입과 동일한 _r 로컬 재구현 패턴을
libs/HAL크라우니.한선 프레임버퍼 API에 적용하면 스폰 3/4회 → 0회까지
갈 수 있음(별도 작업으로 분리 권장, 페이지렌더러는 다른 앱들도 공유하는
범용 컴포넌트라 영향범위가 큼).실패 모드 (배선 시 반드시 고려)
- 서버 다운/미기동:
connect()실패 → 즉시 기존 spawn 경로로 폴백
- 포트 충돌: 여러 브라우저 인스턴스/탭이 같은 고정 포트를 다투면
crowny-ports.sh free 로 탭마다 별도 포트 할당하거나,
포트 1개를 프로세스 singleton으로 공유(멀티탭 동시접속은 이 서버가
단일스레드 accept 루프라 순차 처리되지만, 응답이 수 ms 라 문제 없음 —
단 동시 렌더+키입력이 겹치면 뒤 요청이 대기).
- recv 타임아웃:
CROWNY_TCP_RCVTIMEO_MS미설정 시 5000ms 기본 —
- stale 상태 파일: 서버 프로세스가 죽었다 살아나는 사이 다른 spawn
/tmp/입력상태.psv 를 갱신했다면 — 파일이 SSOT이므로 문제
없음(재검증됨). 단 /tmp/터미널.cmd//tmp/히트맵.psv 는 렌더 요청
시작 시 쓰기(경로,"") 로 초기화하므로, 두 렌더 요청이 동시에 오면
경합 가능(단일 accept 루프라 실제로는 직렬화되어 안전).
- VM 함정 재확인:
TCP읽기(연결)를 1-인자로 호출하면 인자 언더플로로
ANR서버.한선 도 같은 1-인자 호출을 쓰고 있어 잠재 버그로 의심됨,
별도 확인 필요). 반드시 TCP읽기(연결, 최대바이트) 2-인자로 호출.잔여 이슈
libs/ANR서버.한선등 기존 서버들이TCP읽기(연결)를 1-인자로 호출하는
TCP_READ 는 [fd, 최대] 2-인자 스펙) —
다른 상주 서버들도 실제로는 요청 바디를 못 읽고 있을 수 있어 별도 점검
필요(이번 세션 범위 밖, 메모로만 남김).
- 브라우저 실제 배선(crowny-browser.m 수정)은 미착수 — 위 "배선 계획"
- 페이지렌더러(PPM) 상주화는 별도 프로토타입 필요(프레임버퍼 상태 보존